"""This module contains the implementation for locators."""
from matplotlib.ticker import AutoMinorLocator, Locator
import numpy as np
from .constants import SC_EPSILON, SC_INFINITY
from .utils import ang_to_c
__all__ = ["MajorXLocator", "MajorYLocator", "MinorLocator"]
[docs]
class MajorXLocator(Locator):
"""
A locator for the real (resistance/X) axis on the Smith chart.
Attributes:
axes (SmithAxes): The parent Smith chart axes to which this locator applies.
steps (int): The maximum number of divisions for the real axis.
precision (int): The maximum number of significant decimals for tick rounding.
ticks (list): The calculated tick positions.
"""
def __init__(self, axes, n, precision=None):
"""Initialize the MajorXLocator."""
super().__init__()
self.axes = axes
self.steps = n
self.precision = precision if precision is not None else axes._get_key("grid.locator.precision")
if self.precision <= 0:
raise ValueError("`precision` must be greater than 0.")
self.ticks = None
[docs]
def __call__(self):
"""Compute or return cached tick values."""
if self.ticks is None:
self.ticks = self.tick_values(0, SC_INFINITY)
return self.ticks
[docs]
def nice_round(self, num, down=True):
"""
Round a number to a nicely rounded value based on precision.
The rounding behavior adapts dynamically to ensure ticks are visually
consistent across different scales.
Args:
num (float): The number to round.
down (bool, optional): Whether to round down. Defaults to `True`.
Returns: A nicely rounded value.
"""
exp = np.ceil(np.log10(np.abs(num) + SC_EPSILON))
if exp < 1:
exp += 1
norm = 10 ** (-(exp - self.precision))
num_normed = num * norm
if num_normed < 3.3:
norm *= 2
elif num_normed > 50:
norm /= 10
if not 1 < num_normed % 10 < 9:
if abs(num_normed % 10 - 1) < SC_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
[docs]
def tick_values(self, vmin, vmax):
"""
Compute the tick values for the real axis.
Includes the center value as a mandatory tick and dynamically
adjusts spacing to ensure evenly distributed ticks.
Args:
vmin (float): The minimum value of the axis.
vmax (float): The maximum value of the axis.
Returns: he computed tick values for the real axis.
"""
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)
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
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)))
[docs]
def out_of_range(self, x):
"""Check if a value is outside the valid range for the real axis."""
return abs(x) > 1
[docs]
def invert(self, x):
"""Apply the inverse Möbius transformation to a value."""
return self.axes.moebius_inv_z(x)
[docs]
class MajorYLocator(MajorXLocator):
"""
Locator for the imaginary (reactance/Y) axis of a Smith chart.
This class generates evenly spaced, nicely rounded tick values for the imaginary
axis of a Smith chart. It extends the `MajorXLocator` class and adapts it for
handling reactance values.
"""
def __init__(self, axes, n, precision=None):
"""Initialize the MajorYLocator."""
super().__init__(axes, n // 2, precision)
[docs]
def __call__(self):
"""Compute or return cached tick values for the imaginary axis."""
if self.ticks is None:
tmp = self.tick_values(0, SC_INFINITY)
self.ticks = np.concatenate((-tmp[:0:-1], tmp))
return self.ticks
[docs]
def out_of_range(self, x):
"""Check if a value is outside the valid range for the imaginary axis."""
return not 0 <= x <= np.pi
[docs]
def invert(self, x):
"""Apply the inverse Möbius transformation to a value."""
return np.imag(-self.axes.moebius_inv_z(ang_to_c(np.pi + np.array(x))))
[docs]
class MinorLocator(AutoMinorLocator):
"""
Minor tick locator for Smith chart axes.
This locator generates evenly spaced minor ticks between major tick values.
It supports both fixed and automatic modes:
- Fixed mode (n=int): Uses the same number of divisions for all intervals
- Automatic mode (n=None): Adapts divisions per interval based on spacing
Attributes:
ndivs (int or None): The number of divisions between major tick intervals,
or None for automatic mode.
_ticks (numpy.ndarray or None): Cached array of computed minor tick values.
Args:
n (int or None, optional):
The number of divisions between major tick values.
- If an integer: use exactly that many divisions for all intervals.
- If None: automatically compute divisions per interval based on spacing.
Defaults to None (automatic).
"""
def __init__(self, n=None):
"""
Initialize the MinorLocator.
Args:
n (int or None, optional):
The number of divisions between major tick values.
- If an integer: use exactly that many divisions for all intervals.
- If None: automatically compute divisions per interval based on spacing.
Must be a positive integer if provided. Defaults to None (automatic).
"""
if n is not None:
assert isinstance(n, int) and n > 0
super().__init__(n=n)
self._ticks = None
[docs]
def tick_values(self, vmin, vmax):
"""
Call parent to find tick values.
This doesn't get used.
"""
[docs]
def __call__(self):
"""Compute and return minor tick positions.
Ticks are recomputed on every call to ensure they stay synchronized
with the current ndivs setting and major tick locations.
If ndivs is None (automatic mode), divisions are computed adaptively
per interval to maintain uniform spacing within each major interval.
"""
locs = self.axis.get_majorticklocs()
if self.ndivs is None:
# Automatic mode: compute divisions per interval based on span
minor_ticks = []
for p0, p1 in zip(locs[:-1], locs[1:]):
span = abs(p1 - p0)
# Choose divisions based on span magnitude
# Smaller spans get more divisions for consistent visual density
if span < 0.1:
n = 5
elif span < 0.5:
n = 4
elif span < 2.0:
n = 3
else:
n = 2
# Generate uniform divisions within this interval
interval_ticks = np.linspace(p0, p1, n + 1)[1:-1]
minor_ticks.extend(interval_ticks)
self._ticks = np.array(minor_ticks)
else:
# Fixed mode: use same divisions for all intervals
self._ticks = np.hstack([np.linspace(p0, p1, self.ndivs + 1)[1:-1] for p0, p1 in zip(locs[:-1], locs[1:])])
return self._ticks
[docs]
def get_ticklocs(self):
"""Return the computed minor tick locations without filtering."""
return self._ticks