Source code for pysmithchart.core

"""Core SmithAxes class with initialization and configuration."""

import copy

import numpy as np
import matplotlib as mp
import matplotlib.pyplot as plt
from matplotlib.axes import Axes

from pysmithchart.constants import SC_DEFAULT_PARAMS, RC_DEFAULT_PARAMS
from pysmithchart.constants import SC_NEAR_INFINITY, SC_TWICE_INFINITY, SC_EPSILON
from pysmithchart.constants import NORM_Z_DOMAIN
from pysmithchart.locators import MajorXLocator, MajorYLocator, MinorLocator

# Only export the mixin class, not imported symbols
__all__ = ["AxesCore"]


[docs] class AxesCore: """Core functionality for SmithAxes including initialization and configuration. This class is designed to be used as a mixin with matplotlib.axes.Axes via multiple inheritance. Many methods that appear to be missing (like set_aspect, tick_params, text, etc.) are actually provided by Axes at runtime. """ # pylint: disable=no-member # Methods come from Axes mixin name = "smith"
[docs] @classmethod def get_rc_params(cls): """Gets the default values for matplotlib parameters.""" return RC_DEFAULT_PARAMS.copy()
[docs] def update_scParams(self, sc_dict=None, reset=False, **kwargs): """ Update scParams for the current instance. Args: sc_dict (dict, optional): Dictionary of parameters to update using dot notation. Example: {'grid.Z.major.color': 'blue', 'axes.Z0': 75} reset (bool, optional): If True, resets scParams to default values before updating. **kwargs: Additional key-value pairs (must use dot notation). Raises: KeyError: If an invalid parameter key is provided. Note: Parameters must use dot notation (e.g., 'grid.Z.major.color', not 'grid.Z.major_color'). Use shortcuts like Z0, datatype in __init__ instead. """ if reset: self.scParams = copy.deepcopy(SC_DEFAULT_PARAMS) # Merge sc_dict into kwargs for unified processing if sc_dict is not None: kwargs.update(sc_dict) # Process all parameters for key, value in kwargs.items(): if key in self.scParams: self.scParams[key] = value # Handle color auto-propagation if key == "grid.Z.major.color": self.scParams["grid.Z.major.color.x"] = value self.scParams["grid.Z.major.color.y"] = value elif key == "grid.Z.minor.color": self.scParams["grid.Z.minor.color.x"] = value self.scParams["grid.Z.minor.color.y"] = value else: raise KeyError(f"'{key}' is not a valid scParams key. Use dot notation (e.g., 'grid.Z.major.color')")
def __init__(self, *args, **kwargs): """ Initializes a new instance of the `SmithAxes` class. This constructor builds a Smith chart as a custom Matplotlib axes projection. Args: *args: Positional arguments passed to matplotlib.axes.Axes **kwargs: Keyword arguments for Smith chart configuration Essential Shortcuts: Z0 (float): Reference impedance (default: 50Ω) domain (str): Default data domain (Z_DOMAIN, R_DOMAIN, etc.) grid (str): Grid type selection. Options: - 'impedance' (default): Impedance grid only - 'admittance': Admittance grid only - 'both': Both impedance and admittance grids smith_style (dict): Dictionary of Smith chart parameters to modify. Common parameters: 'grid.Z.major.enable': True/False 'grid.Z.minor.enable': True/False 'grid.Z.major.color': 'blue', 'red', etc. 'grid.Z.major.linestyle': '-', '--', ':', etc. 'grid.Z.major.fancy': True/False 'grid.Z.major.fancy.threshold': (50, 50) 'axes.normalize': True/False 'axes.normalize.label': True/False For all available parameters, see SC_DEFAULT_PARAMS in constants.py Examples: >>> # Minimal - just change Z0 >>> fig.add_subplot(111, projection='smith', Z0=75) >>> >>> # Quick admittance chart >>> fig.add_subplot(111, projection='smith', grid='admittance') >>> >>> # Show both impedance and admittance grids >>> fig.add_subplot(111, projection='smith', grid='both') >>> >>> # Recommended approach with smith_style >>> ss = { ... 'grid.Z.major.color': 'blue', ... 'grid.Z.major.fancy.threshold': (50, 50), ... 'grid.Z.minor.enable': True ... } >>> fig.add_subplot(111, projection='smith', smith_style=ss) >>> >>> # Combine shortcuts with smith_style >>> fig.add_subplot(111, projection='smith', ... Z0=75, ... grid='admittance', ... smith_style={'grid.Y.major.color': 'blue'}) """ self._current_zorder = None self._normbox = None self._xaxis_pretransform = None self._xaxis_transform = None self._xaxis_text1_transform = None self._yaxis_stretch = None self._yaxis_correction = None self._yaxis_transform = None self._yaxis_text1_transform = None self._Y_major_arcs = None self._Y_minor_arcs = None self._Z_major_arcs = None self._Z_minor_arcs = None self._Z0 = 50 self.scParams = copy.deepcopy(SC_DEFAULT_PARAMS) self.transProjection = None self.transAffine = None self.transDataToAxes = None self.transAxes = None self.transMoebius = None self.transData = None self.xaxis = None self.yaxis = None # Define shortcut mappings for user-friendly names SHORTCUT_MAP = { "Z0": "axes.Z0", "domain": "plot.default.domain", } # default is a smith chart with impedance grid sc_params_to_set = { "grid.Z.major.enable": True, "grid.Z.minor.enable": True, "grid.Y.major.enable": False, "grid.Y.minor.enable": False, } grid = kwargs.pop("grid", None) if grid is not None and grid != "impedance": grid = grid.lower() if grid == "admittance": sc_params_to_set.update( { "grid.Z.major.enable": False, "grid.Z.minor.enable": False, "grid.Y.major.enable": True, "grid.Y.minor.enable": True, } ) elif grid == "both": sc_params_to_set.update( { "grid.Y.major.enable": True, "grid.Y.minor.enable": True, } ) else: raise ValueError(f"Invalid 'grid' parameter: '{grid}'. Must be 'impedance', 'admittance', or 'both'.") if "smith_style" in kwargs: smith_style = kwargs.pop("smith_style") if not isinstance(smith_style, dict): raise TypeError("smith_style must be a dictionary") sc_params_to_set.update(smith_style) # Process shortcuts second for shortcut, internal_key in SHORTCUT_MAP.items(): if shortcut in kwargs: sc_params_to_set[internal_key] = kwargs.pop(shortcut) # Separate matplotlib axes parameters from Smith chart parameters axes_kwargs = {} for key, value in list(kwargs.items()): # Check if this is a Smith chart parameter (uses dot notation) if "." in key: # Direct dot notation - use as-is sc_params_to_set[key] = kwargs.pop(key) elif key in RC_DEFAULT_PARAMS: # It's a matplotlib rcParam - leave in kwargs for Axes continue else: # Not a known Smith chart param or rcParam - pass to Axes axes_kwargs[key] = kwargs.pop(key) # Apply Smith chart parameters if sc_params_to_set: self.update_scParams(sc_dict=sc_params_to_set) if self._get_key("init.updaterc"): for key, value in RC_DEFAULT_PARAMS.items(): if mp.rcParams[key] == mp.rcParamsDefault[key]: mp.rcParams[key] = value Axes.__init__(self, *args, **axes_kwargs) # pylint: disable=non-parent-init-called self.set_aspect(1, adjustable="box", anchor="C") self.tick_params(axis="both", which="both", bottom=False, top=False, left=False, right=False) def _get_key(self, key): """Get value for key from the local dictionary or the global matplotlib rcParams.""" if key in self.scParams: return self.scParams[key] if key in mp.rcParams: return mp.rcParams[key] 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 _init_smith_chart(self): """ Initialize Smith chart-specific configuration. Called once during __init__ to set up locators, formatters, limits, and other Smith chart-specific properties. """ # Reset state self._normbox = None self._Z0 = self._get_key("axes.Z0") self._current_zorder = self._get_key("plot.zorder") # Set limits first (before locators which may trigger updates) Axes.set_xlim(self, 0, SC_TWICE_INFINITY) Axes.set_ylim(self, -SC_TWICE_INFINITY, SC_TWICE_INFINITY) # Configure axis locators # Use admittance divisions if only admittance is enabled, otherwise use impedance impedance_enabled = self._get_key("grid.Z.major.enable") admittance_enabled = self._get_key("grid.Y.major.enable") if admittance_enabled and not impedance_enabled: # Pure admittance chart - use Y divisions real_divs = self._get_key("grid.Y.major.real.divisions") imag_divs = self._get_key("grid.Y.major.imag.divisions") real_minor_divs = self._get_key("grid.Y.minor.real.divisions") imag_minor_divs = self._get_key("grid.Y.minor.imag.divisions") else: # Impedance chart or both - use Z divisions real_divs = self._get_key("grid.Z.major.real.divisions") imag_divs = self._get_key("grid.Z.major.imag.divisions") real_minor_divs = self._get_key("grid.Z.minor.real.divisions") imag_minor_divs = self._get_key("grid.Z.minor.imag.divisions") self.xaxis.set_major_locator(MajorXLocator(self, real_divs)) self.yaxis.set_major_locator(MajorYLocator(self, imag_divs)) self.xaxis.set_minor_locator(MinorLocator(real_minor_divs)) self.yaxis.set_minor_locator(MinorLocator(imag_minor_divs)) # Configure ticks self.xaxis.set_ticks_position("none") self.yaxis.set_ticks_position("none") # Configure x-axis labels (resistance/conductance) # Turn off matplotlib's automatic tick labels self.xaxis.set_major_formatter(plt.NullFormatter()) self.yaxis.set_major_formatter(plt.NullFormatter()) # Manually add labels with full control over positioning self._add_manual_axis_labels() def _add_manual_axis_labels(self): """Manually add axis labels for both impedance and admittance modes.""" bbox = self._get_key("axes.xlabel.fancybox") rotation = self._get_key("axes.xlabel.rotation") # Determine grid mode impedance_enabled = self._get_key("grid.Z.major.enable") admittance_enabled = self._get_key("grid.Y.major.enable") # Get tick locations x_major_locs = self.xaxis.get_majorticklocs() y_major_locs = self.yaxis.get_majorticklocs() # Add X-axis (real axis) labels, x_pos is in the NORM_Z_DOMAIN for loc in x_major_locs: if admittance_enabled and not impedance_enabled: if loc < SC_EPSILON: label_text = "∞" x_pos = 0 # right edge on admittance chart elif loc >= SC_NEAR_INFINITY: label_text = "0" x_pos = SC_NEAR_INFINITY # left edge on admittance chart else: label_text = ("%f" % loc).rstrip("0").rstrip(".") x_pos = 1 / loc # Reciprocal position else: if loc < SC_EPSILON: label_text = "0" x_pos = 0 # Left edge elif loc >= SC_NEAR_INFINITY: label_text = "∞" x_pos = SC_NEAR_INFINITY # Right edge else: label_text = ("%f" % loc).rstrip("0").rstrip(".") x_pos = loc # Place label at bottom of chart where circle crosses real axis self.text( x_pos, 0, label_text, verticalalignment="center", horizontalalignment="center", rotation_mode="anchor", rotation=rotation, bbox=bbox, domain=NORM_Z_DOMAIN, clip_on=False, ) # Add Y-axis (imaginary axis) labels, y_pos is in the NORM_Z_DOMAIN for loc in y_major_locs: if abs(loc) < SC_EPSILON or abs(loc) >= SC_NEAR_INFINITY: continue # Regular value - format and position label_text = ("%f" % abs(loc)).rstrip("0").rstrip(".") + "j" if loc < 0: label_text = "-" + label_text # Position in NORM_Z_DOMAIN (normalized impedance space) # For admittance, label at reciprocal position if admittance_enabled and not impedance_enabled: y_pos = 1 / loc else: y_pos = loc # Calculate horizontal position for alignment x_moebius = np.real(self.moebius_z(y_pos * 1j)) if x_moebius < -0.1: ha = "right" elif x_moebius > 0.1: ha = "left" else: ha = "center" # Vertical alignment based on top/bottom half # Positive y_pos = top half, negative y_pos = bottom half if y_pos > 0: va = "bottom" # Label below the point (top half of chart) elif y_pos < 0: va = "top" # Label above the point (bottom half of chart) else: va = "center" # Center label (at y=0) # Place label using NORM_Z_DOMAIN (no special size for infinity) self.text( 0, y_pos, label_text, verticalalignment=va, horizontalalignment=ha, domain=NORM_Z_DOMAIN, clip_on=False, ) # Add normalization label if enabled if self._get_key("axes.normalize") and self._get_key("axes.normalize.label"): x = self._get_key("axes.normalize.label.position.x") y = self._get_key("axes.normalize.label.position.y") s = "Z₀ = %d Ω" % self._get_key("axes.Z0") self.text(x, y, s, fontsize=14, transform=self.transAxes) # Enable grids according to settings - grid() checks the enable flags internally self.grid(grid="both")
[docs] def clear(self): """ Clear the Smith chart axes. Resets the chart to a clean state. Called automatically during __init__ and when user explicitly calls plt.cla(). """ # Reset Smith chart-specific state self._Z_major_arcs = [] self._Z_minor_arcs = [] self._Y_major_arcs = [] self._Y_minor_arcs = [] self._normbox = None # Temporarily disable grid to prevent issues during parent clear original_grid = getattr(self, "grid", None) if original_grid is not None: self.grid = lambda *args, **kwargs: None try: Axes.clear(self) finally: if original_grid is not None: self.grid = original_grid # Perform Smith chart initialization self._init_smith_chart()