Skip to content

Commit 308c01d

Browse files
committed
feat: add mil and inch units to plot_length_units
1 parent 89bbe6e commit 308c01d

File tree

5 files changed

+110
-5
lines changed

5 files changed

+110
-5
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
- Added "mil" and "in" (inch) units to `plot_length_units`.
12+
1013
### Changed
1114
- By default, batch downloads will skip files that already exist locally. To force re-downloading and replace existing files, pass the `replace_existing=True` argument to `Batch.load()`, `Batch.download()`, or `BatchData.load()`.
1215
- The `BatchData.load_sim_data()` function now overwrites any previously downloaded simulation files (instead of skipping them).

tests/test_components/test_geometry.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,8 @@ def test_plot(component):
9696
def test_plot_with_units():
9797
_ = BOX.plot(z=0, ax=AX, plot_length_units="nm")
9898
plt.close()
99+
_ = BOX.plot(z=0, ax=AX, plot_length_units="mil")
100+
plt.close()
99101

100102

101103
def test_base_inside():

tidy3d/components/types.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@ def __modify_schema__(cls, field_schema):
199199
PlanePosition = Literal["bottom", "middle", "top"]
200200
ClipOperationType = Literal["union", "intersection", "difference", "symmetric_difference"]
201201
BoxSurface = Literal["x-", "x+", "y-", "y+", "z-", "z+"]
202-
LengthUnit = Literal["nm", "μm", "um", "mm", "cm", "m"]
202+
LengthUnit = Literal["nm", "μm", "um", "mm", "cm", "m", "mil", "in"]
203203
PriorityMode = Literal["equal", "conductor"]
204204

205205
""" medium """

tidy3d/components/viz/axes_utils.py

Lines changed: 103 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,95 @@
88
from tidy3d.exceptions import Tidy3dKeyError
99

1010

11+
def _create_unit_aware_locator():
12+
"""Create UnitAwareLocator lazily due to matplotlib import restrictions."""
13+
import matplotlib.ticker as ticker
14+
15+
class UnitAwareLocator(ticker.Locator):
16+
"""Custom tick locator that places ticks at nice positions in the target unit."""
17+
18+
def __init__(self, scale_factor: float):
19+
"""
20+
Parameters
21+
----------
22+
scale_factor : float
23+
Factor to convert from micrometers to the target unit.
24+
"""
25+
super().__init__()
26+
self.scale_factor = scale_factor
27+
28+
def __call__(self):
29+
vmin, vmax = self.axis.get_view_interval()
30+
return self.tick_values(vmin, vmax)
31+
32+
def view_limits(self, vmin, vmax):
33+
"""Override to prevent matplotlib from adjusting our limits."""
34+
return vmin, vmax
35+
36+
def tick_values(self, vmin, vmax):
37+
# convert the view range to the target unit
38+
vmin_unit = vmin * self.scale_factor
39+
vmax_unit = vmax * self.scale_factor
40+
41+
# tolerance for floating point comparisons in target unit
42+
unit_range = vmax_unit - vmin_unit
43+
unit_tol = unit_range * 1e-8
44+
45+
locator = ticker.MaxNLocator(nbins=11, prune=None, min_n_ticks=2)
46+
47+
ticks_unit = locator.tick_values(vmin_unit, vmax_unit)
48+
49+
# ensure we have ticks that cover the full range
50+
if len(ticks_unit) > 0:
51+
if ticks_unit[0] > vmin_unit + unit_tol or ticks_unit[-1] < vmax_unit - unit_tol:
52+
# try with fewer bins to get better coverage
53+
for n in [10, 9, 8, 7, 6, 5]:
54+
locator = ticker.MaxNLocator(nbins=n, prune=None, min_n_ticks=2)
55+
ticks_unit = locator.tick_values(vmin_unit, vmax_unit)
56+
if (
57+
len(ticks_unit) >= 3
58+
and ticks_unit[0] <= vmin_unit + unit_tol
59+
and ticks_unit[-1] >= vmax_unit - unit_tol
60+
):
61+
break
62+
63+
# if still no good coverage, manually ensure edge coverage
64+
if len(ticks_unit) > 0:
65+
if (
66+
ticks_unit[0] > vmin_unit + unit_tol
67+
or ticks_unit[-1] < vmax_unit - unit_tol
68+
):
69+
# find a reasonable step size from existing ticks
70+
if len(ticks_unit) > 1:
71+
step = ticks_unit[1] - ticks_unit[0]
72+
else:
73+
step = unit_range / 5
74+
75+
# extend the range to ensure coverage
76+
extended_min = vmin_unit - step
77+
extended_max = vmax_unit + step
78+
79+
# try one more time with extended range
80+
locator = ticker.MaxNLocator(nbins=8, prune=None, min_n_ticks=2)
81+
ticks_unit = locator.tick_values(extended_min, extended_max)
82+
83+
# filter to reasonable bounds around the original range
84+
ticks_unit = [
85+
t
86+
for t in ticks_unit
87+
if t >= vmin_unit - step / 2 and t <= vmax_unit + step / 2
88+
]
89+
90+
# convert the nice ticks back to the original data unit (micrometers)
91+
ticks_um = ticks_unit / self.scale_factor
92+
93+
# filter to ensure ticks are within bounds (with small tolerance)
94+
eps = (vmax - vmin) * 1e-8
95+
return [tick for tick in ticks_um if vmin - eps <= tick <= vmax + eps]
96+
97+
return UnitAwareLocator
98+
99+
11100
def make_ax() -> Ax:
12101
"""makes an empty ``ax``."""
13102
import matplotlib.pyplot as plt
@@ -72,14 +161,24 @@ def set_default_labels_and_title(
72161
)
73162
ax.set_xlabel(f"{xlabel} ({plot_length_units})")
74163
ax.set_ylabel(f"{ylabel} ({plot_length_units})")
75-
# Formatter to help plot in arbitrary units
164+
76165
scale_factor = UnitScaling[plot_length_units]
166+
167+
# for imperial units, use custom tick locator for nice tick positions
168+
if plot_length_units in ["mil", "in"]:
169+
UnitAwareLocator = _create_unit_aware_locator()
170+
x_locator = UnitAwareLocator(scale_factor)
171+
y_locator = UnitAwareLocator(scale_factor)
172+
ax.xaxis.set_major_locator(x_locator)
173+
ax.yaxis.set_major_locator(y_locator)
174+
77175
formatter = ticker.FuncFormatter(lambda y, _: f"{y * scale_factor:.2f}")
176+
78177
ax.xaxis.set_major_formatter(formatter)
79178
ax.yaxis.set_major_formatter(formatter)
80-
ax.set_title(
81-
f"cross section at {'xyz'[axis]}={position * scale_factor:.2f} ({plot_length_units})"
82-
)
179+
180+
position_scaled = position * scale_factor
181+
ax.set_title(f"cross section at {'xyz'[axis]}={position_scaled:.2f} ({plot_length_units})")
83182
else:
84183
ax.set_xlabel(xlabel)
85184
ax.set_ylabel(ylabel)

tidy3d/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,7 @@
245245
"mm": 1e-3,
246246
"cm": 1e-4,
247247
"m": 1e-6,
248+
"mil": 1.0 / 25.4,
248249
"in": 1.0 / 25400,
249250
}
250251
)

0 commit comments

Comments
 (0)