Skip to content

Commit 717c064

Browse files
committed
feat[viz]: fill argument for sim.plot_structures()
1 parent 4bb5368 commit 717c064

File tree

5 files changed

+131
-16
lines changed

5 files changed

+131
-16
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+
- `fill` and `fill_structures` argument in `td.Simulation.plot_structures()` and `td.Simulation.plot()` respectively to disable fill and plot outlines of structures only.
12+
1013
## [2.8.1] - 2025-03-20
1114

1215
### Added

tests/test_components/test_viz.py

Lines changed: 68 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,26 @@
11
"""Tests visualization operations."""
22

3+
import matplotlib as mpl
34
import matplotlib.pyplot as plt
45
import pydantic.v1 as pd
56
import pytest
67
import tidy3d as td
8+
from tidy3d import Box, Medium, Simulation, Structure
79
from tidy3d.components.viz import Polygon, set_default_labels_and_title
810
from tidy3d.constants import inf
911
from tidy3d.exceptions import Tidy3dKeyError
1012

1113

14+
@pytest.fixture(scope="module", autouse=True)
15+
def mpl_config():
16+
"""Configure matplotlib non-interactive backend for all tests in this module."""
17+
original_backend = mpl.get_backend()
18+
mpl.use("Agg")
19+
yield
20+
plt.close("all")
21+
mpl.use(original_backend)
22+
23+
1224
def test_make_polygon_dict():
1325
p = Polygon(context={"coordinates": [(1, 0), (0, 1), (0, 0)]})
1426
p.interiors
@@ -38,8 +50,6 @@ def test_0d_plot(center_z, len_collections):
3850
# if a point is plotted, a single collection will be present, otherwise nothing
3951
assert len(ax.collections) == len_collections
4052

41-
plt.close()
42-
4353

4454
def test_2d_boundary_plot():
4555
"""
@@ -102,8 +112,6 @@ def test_set_default_labels_title():
102112
axis_labels=axis_labels, axis=2, position=0, ax=ax, plot_length_units="inches"
103113
)
104114

105-
plt.close()
106-
107115

108116
def test_make_viz_spec():
109117
"""
@@ -144,7 +152,6 @@ def test_plot_from_structure():
144152
structure = td.Structure(geometry=geometry, medium=medium)
145153

146154
structure.plot(z=0)
147-
plt.close()
148155

149156

150157
def test_plot_from_simulation():
@@ -158,7 +165,6 @@ def test_plot_from_simulation():
158165
)
159166

160167
refine_box.plot(z=0)
161-
plt.close()
162168

163169

164170
def plot_with_viz_spec(alpha, facecolor, edgecolor=None, use_viz_spec=True):
@@ -263,3 +269,59 @@ def test_plot_multi_from_structure_local(rng):
263269
rng=rng,
264270
use_viz_spec=False,
265271
)
272+
273+
274+
def test_sim_plot_fill_structures():
275+
"""Test fill_structures in Simulation.plot()"""
276+
box = Box(size=(1, 1, 1))
277+
struct = Structure(geometry=box, medium=Medium(permittivity=2.0))
278+
sim = Simulation(
279+
size=(2, 2, 2),
280+
structures=[struct],
281+
grid_spec=td.GridSpec(wavelength=1.0),
282+
run_time=1e-12,
283+
)
284+
285+
fig1, ax1 = plt.subplots()
286+
sim.plot(x=0, fill_structures=False, ax=ax1)
287+
structure_patches = [p for p in ax1.patches if isinstance(p, mpl.patches.PathPatch)]
288+
for patch in structure_patches[:1]: # only one structure, rest is PML etc
289+
assert not patch.get_fill(), "Should be unfilled when False"
290+
assert patch.get_edgecolor() != "none"
291+
292+
fig2, ax2 = plt.subplots()
293+
sim.plot(x=0, fill_structures=True, ax=ax2)
294+
structure_patches = [p for p in ax2.patches if isinstance(p, mpl.patches.PathPatch)]
295+
for patch in structure_patches[:1]:
296+
assert patch.get_fill(), "Should be filled when True"
297+
298+
299+
def test_sim_plot_structures_fill():
300+
"""Test fill_structures in Simulation.plot_structures()"""
301+
box = Box(size=(1, 1, 1))
302+
struct = Structure(geometry=box, medium=Medium(permittivity=2.0))
303+
sim = Simulation(
304+
size=(2, 2, 2),
305+
structures=[struct],
306+
grid_spec=td.GridSpec(wavelength=1.0),
307+
run_time=1e-12,
308+
)
309+
310+
fig1, ax1 = plt.subplots()
311+
sim.plot_structures(x=0, fill=False, ax=ax1)
312+
structure_patches = [p for p in ax1.patches if isinstance(p, mpl.patches.PathPatch)]
313+
assert len(structure_patches) > 0, "No structures plotted"
314+
315+
for patch in structure_patches[:1]:
316+
assert not patch.get_fill(), "Should be unfilled when False"
317+
assert patch.get_edgecolor() != "none", "Edges should be visible"
318+
assert patch.get_linewidth() > 0, "Edge width should be positive"
319+
320+
fig2, ax2 = plt.subplots()
321+
sim.plot_structures(x=0, fill=True, ax=ax2)
322+
structure_patches = [p for p in ax2.patches if isinstance(p, mpl.patches.PathPatch)]
323+
assert len(structure_patches) > 0, "No structures plotted"
324+
325+
for patch in structure_patches[:1]:
326+
assert patch.get_fill(), "Should be filled when True"
327+
assert patch.get_facecolor() != "none", "Face color should be set"

tidy3d/components/base_sim/simulation.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,7 @@ def plot(
228228
monitor_alpha: float = None,
229229
hlim: Tuple[float, float] = None,
230230
vlim: Tuple[float, float] = None,
231+
fill_structures: bool = True,
231232
**patch_kwargs,
232233
) -> Ax:
233234
"""Plot each of simulation's components on a plane defined by one nonzero x,y,z coordinate.
@@ -250,7 +251,8 @@ def plot(
250251
The x range if plotting on xy or xz planes, y range if plotting on yz plane.
251252
vlim : Tuple[float, float] = None
252253
The z range if plotting on xz or yz planes, y plane if plotting on xy plane.
253-
254+
fill_structures : bool = True
255+
Whether to fill structures with color or just draw outlines.
254256
Returns
255257
-------
256258
matplotlib.axes._subplots.Axes
@@ -261,7 +263,9 @@ def plot(
261263
bounds=self.simulation_bounds, x=x, y=y, z=z, hlim=hlim, vlim=vlim
262264
)
263265

264-
ax = self.scene.plot_structures(ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim)
266+
ax = self.scene.plot_structures(
267+
ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim, fill=fill_structures
268+
)
265269
ax = self.plot_sources(ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim, alpha=source_alpha)
266270
ax = self.plot_monitors(ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim, alpha=monitor_alpha)
267271
ax = Scene._set_plot_bounds(
@@ -492,6 +496,7 @@ def plot_structures(
492496
ax: Ax = None,
493497
hlim: Tuple[float, float] = None,
494498
vlim: Tuple[float, float] = None,
499+
fill: bool = True,
495500
) -> Ax:
496501
"""Plot each of simulation's structures on a plane defined by one nonzero x,y,z coordinate.
497502
@@ -509,7 +514,8 @@ def plot_structures(
509514
The x range if plotting on xy or xz planes, y range if plotting on yz plane.
510515
vlim : Tuple[float, float] = None
511516
The z range if plotting on xz or yz planes, y plane if plotting on xy plane.
512-
517+
fill : bool = True
518+
Whether to fill structures with color or just draw outlines.
513519
Returns
514520
-------
515521
matplotlib.axes._subplots.Axes
@@ -520,7 +526,9 @@ def plot_structures(
520526
bounds=self.simulation_bounds, x=x, y=y, z=z, hlim=hlim, vlim=vlim
521527
)
522528

523-
return self.scene.plot_structures(x=x, y=y, z=z, ax=ax, hlim=hlim_new, vlim=vlim_new)
529+
return self.scene.plot_structures(
530+
x=x, y=y, z=z, ax=ax, hlim=hlim_new, vlim=vlim_new, fill=fill
531+
)
524532

525533
@equal_aspect
526534
@add_ax_if_none

tidy3d/components/scene.py

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,7 @@ def plot(
372372
ax: Ax = None,
373373
hlim: Tuple[float, float] = None,
374374
vlim: Tuple[float, float] = None,
375+
fill_structures: bool = True,
375376
**patch_kwargs,
376377
) -> Ax:
377378
"""Plot each of scene's components on a plane defined by one nonzero x,y,z coordinate.
@@ -390,6 +391,8 @@ def plot(
390391
The x range if plotting on xy or xz planes, y range if plotting on yz plane.
391392
vlim : Tuple[float, float] = None
392393
The z range if plotting on xz or yz planes, y plane if plotting on xy plane.
394+
fill_structures : bool = True
395+
Whether to fill structures with color or just draw outlines.
393396
394397
Returns
395398
-------
@@ -399,7 +402,7 @@ def plot(
399402

400403
hlim, vlim = Scene._get_plot_lims(bounds=self.bounds, x=x, y=y, z=z, hlim=hlim, vlim=vlim)
401404

402-
ax = self.plot_structures(ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim)
405+
ax = self.plot_structures(ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim, fill=fill_structures)
403406
ax = self._set_plot_bounds(bounds=self.bounds, ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim)
404407
return ax
405408

@@ -413,6 +416,7 @@ def plot_structures(
413416
ax: Ax = None,
414417
hlim: Tuple[float, float] = None,
415418
vlim: Tuple[float, float] = None,
419+
fill: bool = True,
416420
) -> Ax:
417421
"""Plot each of scene's structures on a plane defined by one nonzero x,y,z coordinate.
418422
@@ -430,6 +434,8 @@ def plot_structures(
430434
The x range if plotting on xy or xz planes, y range if plotting on yz plane.
431435
vlim : Tuple[float, float] = None
432436
The z range if plotting on xz or yz planes, y plane if plotting on xy plane.
437+
fill : bool = True
438+
Whether to fill structures with color or just draw outlines.
433439
434440
Returns
435441
-------
@@ -443,7 +449,13 @@ def plot_structures(
443449
medium_map = self.medium_map
444450
for medium, shape in medium_shapes:
445451
mat_index = medium_map[medium]
446-
ax = self._plot_shape_structure(medium=medium, mat_index=mat_index, shape=shape, ax=ax)
452+
ax = self._plot_shape_structure(
453+
medium=medium,
454+
mat_index=mat_index,
455+
shape=shape,
456+
ax=ax,
457+
fill=fill,
458+
)
447459

448460
# clean up the axis display
449461
axis, _ = Box.parse_xyz_kwargs(x=x, y=y, z=z)
@@ -456,15 +468,27 @@ def plot_structures(
456468
return ax
457469

458470
def _plot_shape_structure(
459-
self, medium: MultiPhysicsMediumType3D, mat_index: int, shape: Shapely, ax: Ax
471+
self,
472+
medium: MultiPhysicsMediumType3D,
473+
mat_index: int,
474+
shape: Shapely,
475+
ax: Ax,
476+
fill: bool = True,
460477
) -> Ax:
461478
"""Plot a structure's cross section shape for a given medium."""
462-
plot_params_struct = self._get_structure_plot_params(medium=medium, mat_index=mat_index)
479+
plot_params_struct = self._get_structure_plot_params(
480+
medium=medium,
481+
mat_index=mat_index,
482+
fill=fill,
483+
)
463484
ax = self.box.plot_shape(shape=shape, plot_params=plot_params_struct, ax=ax)
464485
return ax
465486

466487
def _get_structure_plot_params(
467-
self, mat_index: int, medium: MultiPhysicsMediumType3D
488+
self,
489+
mat_index: int,
490+
medium: MultiPhysicsMediumType3D,
491+
fill: bool = True,
468492
) -> PlotParams:
469493
"""Constructs the plot parameters for a given medium in scene.plot()."""
470494

@@ -508,6 +532,11 @@ def _get_structure_plot_params(
508532
if medium.viz_spec is not None:
509533
plot_params = plot_params.override_with_viz_spec(medium.viz_spec)
510534

535+
if not fill:
536+
plot_params = plot_params.copy(update={"fill": False})
537+
if plot_params.linewidth == 0:
538+
plot_params = plot_params.copy(update={"linewidth": 1})
539+
511540
return plot_params
512541

513542
@staticmethod

tidy3d/components/simulation.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -385,12 +385,15 @@ def plot(
385385
lumped_element_alpha: float = None,
386386
hlim: Tuple[float, float] = None,
387387
vlim: Tuple[float, float] = None,
388+
fill_structures: bool = True,
388389
**patch_kwargs,
389390
) -> Ax:
390391
"""Plot each of simulation's components on a plane defined by one nonzero x,y,z coordinate.
391392
392393
Parameters
393394
----------
395+
fill_structures : bool = True
396+
Whether to fill structures with color or just draw outlines.
394397
x : float = None
395398
position of plane in x direction, only one of x, y, z must be specified to define plane.
396399
y : float = None
@@ -426,7 +429,16 @@ def plot(
426429
bounds=self.simulation_bounds, x=x, y=y, z=z, hlim=hlim, vlim=vlim
427430
)
428431

429-
ax = self.plot_structures(ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim)
432+
ax = self.scene.plot(
433+
x=x,
434+
y=y,
435+
z=z,
436+
ax=ax,
437+
hlim=hlim,
438+
vlim=vlim,
439+
fill_structures=fill_structures,
440+
)
441+
430442
ax = self.plot_sources(ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim, alpha=source_alpha)
431443
ax = self.plot_monitors(ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim, alpha=monitor_alpha)
432444
ax = self.plot_lumped_elements(
@@ -438,6 +450,7 @@ def plot(
438450
bounds=self.simulation_bounds, ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim
439451
)
440452
ax = self.plot_boundaries(ax=ax, x=x, y=y, z=z)
453+
441454
return ax
442455

443456
@equal_aspect

0 commit comments

Comments
 (0)