Skip to content

Commit a17397f

Browse files
authored
feat[viz]: Add option to plot specific permittivity component of anisotropic media
Add component selection to plot_eps Merge pull request #2315 from flexcompute/bzhangflex/plot-ani-eps
2 parents 98cea00 + 0f302ef commit a17397f

File tree

7 files changed

+449
-38
lines changed

7 files changed

+449
-38
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111
- `fill` and `fill_structures` argument in `td.Simulation.plot_structures()` and `td.Simulation.plot()` respectively to disable fill and plot outlines of structures only.
1212
- New subpixel averaging option `ContourPathAveraging` applied to dielectric material boundaries.
1313
- A property `interior_angle` in `PolySlab` that stores angles formed inside polygon by two adjacent edges.
14+
- `eps_component` argument in `td.Simulation.plot_eps()` to optionally select a specific permittivity component to plot (eg. `"xx"`).
1415

1516
### Fixed
1617
- Compatibility with `xarray>=2025.03`.

tests/test_components/test_simulation.py

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
"""Tests the simulation and its validators."""
22

3+
import shutil
4+
from pathlib import Path
5+
36
import gdstk
47
import matplotlib.pyplot as plt
58
import numpy as np
69
import pydantic.v1 as pydantic
710
import pytest
811
import tidy3d as td
12+
from matplotlib.testing.decorators import check_figures_equal
913
from tidy3d.components import simulation
1014
from tidy3d.components.scene import MAX_GEOMETRY_COUNT, MAX_NUM_MEDIUMS
1115
from tidy3d.components.simulation import MAX_NUM_SOURCES
@@ -693,6 +697,208 @@ def test_plot_eps_bounds():
693697
plt.close()
694698

695699

700+
class TestAnisotropicPlotting:
701+
"""Tests for plotting anisotropic media"""
702+
703+
diag_comps = ["xx", "yy", "zz"]
704+
offdiag_comps = ["xy", "yx", "xz", "zx", "yz", "zy"]
705+
allcomps = diag_comps + offdiag_comps
706+
707+
medium_diag = td.AnisotropicMedium(
708+
xx=td.Medium(permittivity=5), yy=td.Medium(permittivity=10), zz=td.Medium(permittivity=15)
709+
)
710+
711+
medium_fullyani = td.FullyAnisotropicMedium(permittivity=[[6, 2, 3], [2, 7, 4], [3, 4, 8]])
712+
713+
@pytest.fixture(scope="class")
714+
def medium_customani(self):
715+
"""based this custom medium on
716+
https://docs.flexcompute.com/projects/tidy3d/en/latest/api/_autosummary/tidy3d.CustomAnisotropicMedium.html
717+
"""
718+
Nx, Ny, Nz = 100, 100, 100
719+
x = np.linspace(-1, 1, Nx)
720+
y = np.linspace(-1, 1, Ny)
721+
z = np.linspace(-1, 1, Nz)
722+
coords = dict(x=x, y=y, z=z)
723+
permittivity = td.SpatialDataArray(2 * np.ones((Nx, Ny, Nz)), coords=coords)
724+
conductivity = td.SpatialDataArray(np.ones((Nx, Ny, Nz)), coords=coords)
725+
medium_xx = td.CustomMedium(permittivity=permittivity, conductivity=conductivity)
726+
medium_yy = td.CustomMedium(permittivity=2 * permittivity, conductivity=conductivity)
727+
728+
# make the zz component a spatially varying medium
729+
# define coordinate array
730+
x_mesh, y_mesh, _ = np.meshgrid(x, y, z, indexing="ij")
731+
r_mesh = np.sqrt(x_mesh**2 + y_mesh**2) # radial distance
732+
733+
# index of refraction array
734+
# assign the refractive index value to the array according to the desired profile
735+
n_data = np.ones((Nx, Ny, Nz))
736+
n0 = 2
737+
A = 0.5
738+
r = 1
739+
n_data[r_mesh <= r] = n0 * (1 - A * r_mesh[r_mesh <= r] ** 2)
740+
# convert to dataset array
741+
n_dataset = td.SpatialDataArray(n_data, coords=dict(x=x, y=y, z=z))
742+
medium_zz = td.CustomMedium.from_nk(n_dataset, interp_method="nearest")
743+
744+
return td.CustomAnisotropicMedium(xx=medium_xx, yy=medium_yy, zz=medium_zz)
745+
746+
@pytest.fixture(scope="module", autouse=True)
747+
def cleanup_figures(self):
748+
yield # Run tests first
749+
fig_dir = Path().resolve() / "result_images"
750+
shutil.rmtree(fig_dir, ignore_errors=False)
751+
752+
def make_sim(self, medium):
753+
L = 5
754+
755+
source = td.UniformCurrentSource(
756+
center=(0, 0, -L / 3),
757+
size=(L, L / 2, 0),
758+
polarization="Ex",
759+
source_time=td.GaussianPulse(
760+
freq0=td.C_0,
761+
fwidth=10e14,
762+
),
763+
)
764+
structures = (td.Structure(geometry=td.Sphere(center=(0, 0, 0), radius=1), medium=medium),)
765+
766+
return td.Simulation(
767+
size=(L, L, L),
768+
grid_spec=td.GridSpec.uniform(dl=0.01),
769+
structures=structures,
770+
sources=[source],
771+
run_time=1e-12,
772+
)
773+
774+
@pytest.mark.parametrize("eps_comp", ("xyz", "123", "", 5))
775+
def test_bad_eps_arg(self, eps_comp):
776+
"""Tests that an incorrect component raises the proper exception."""
777+
with pytest.raises(ValueError, match=f"eps_component '{eps_comp}' is not supported. "):
778+
self.make_sim(self.medium_diag).plot_eps(x=0, eps_component=eps_comp)
779+
780+
@pytest.mark.parametrize(
781+
"eps_comp",
782+
[
783+
None,
784+
]
785+
+ diag_comps,
786+
)
787+
def test_plot_anisotropic_medium(self, eps_comp):
788+
"""Test plotting diagonal components of a diagonally anisotropic medium succeeds or not.
789+
diagonal components and ``None`` should succeed.
790+
"""
791+
self.make_sim(self.medium_diag).plot_eps(x=0, eps_component=eps_comp)
792+
793+
@pytest.mark.parametrize("eps_comp", offdiag_comps)
794+
def test_plot_anisotropic_medium_offdiagfail(self, eps_comp):
795+
"""Tests that plotting off-diagonal components of a diagonally anisotropic medium raises an exception."""
796+
with pytest.raises(
797+
ValueError,
798+
match=f"Plotting component '{eps_comp}' of a diagonally-anisotropic permittivity tensor is not supported",
799+
):
800+
self.make_sim(self.medium_diag).plot_eps(x=0, eps_component=eps_comp)
801+
802+
@pytest.mark.parametrize(
803+
"eps_comp1,eps_comp2",
804+
(
805+
pytest.param("xx", "yy", marks=pytest.mark.xfail),
806+
pytest.param("xx", "zz", marks=pytest.mark.xfail),
807+
pytest.param("yy", "zz", marks=pytest.mark.xfail),
808+
),
809+
)
810+
@check_figures_equal(extensions=("png",))
811+
def test_plot_anisotropic_medium_diff(self, fig_test, fig_ref, eps_comp1, eps_comp2):
812+
"""Tests that the plots of different components of an AnisotropicMedium are actually different."""
813+
sim = self.make_sim(self.medium_diag)
814+
815+
ax1 = fig_test.add_subplot()
816+
sim.plot_eps(x=0, eps_component=eps_comp1, ax=ax1)
817+
ax2 = fig_ref.add_subplot()
818+
sim.plot_eps(x=0, eps_component=eps_comp2, ax=ax2)
819+
820+
@pytest.mark.parametrize(
821+
"eps_comp",
822+
[
823+
None,
824+
]
825+
+ diag_comps
826+
+ offdiag_comps,
827+
)
828+
def test_plot_fully_anisotropic_medium(self, eps_comp):
829+
"""Test plotting all components of a fully anisotropic medium.
830+
All plots should succeed.
831+
"""
832+
sim = self.make_sim(self.medium_fullyani)
833+
sim.plot_eps(x=0, eps_component=eps_comp)
834+
835+
# Test parameters for comparing plots of a FullyAnisotropicMedium
836+
fullyani_testplot_diff_params = []
837+
for eps_comp1 in allcomps:
838+
for eps_comp2 in allcomps:
839+
if eps_comp1 == eps_comp2 or eps_comp1[::-1] == eps_comp2:
840+
# Same components, or transposed components (eg. xy and yx) should plot the same
841+
fullyani_testplot_diff_params.append((eps_comp1, eps_comp2))
842+
else:
843+
# All other component pairs should plot differently
844+
fullyani_testplot_diff_params.append(
845+
pytest.param(eps_comp1, eps_comp2, marks=pytest.mark.xfail)
846+
)
847+
848+
@pytest.mark.parametrize("eps_comp1,eps_comp2", fullyani_testplot_diff_params)
849+
@check_figures_equal(extensions=("png",))
850+
def test_plot_fully_anisotropic_medium_diff(self, fig_test, fig_ref, eps_comp1, eps_comp2):
851+
"""Tests that the plots of different components of a FullyAnisotropicMedium are actually different."""
852+
sim = self.make_sim(self.medium_fullyani)
853+
854+
ax1 = fig_test.add_subplot()
855+
sim.plot_eps(x=0, eps_component=eps_comp1, ax=ax1)
856+
ax2 = fig_ref.add_subplot()
857+
sim.plot_eps(x=0, eps_component=eps_comp2, ax=ax2)
858+
859+
@pytest.mark.parametrize(
860+
"eps_comp",
861+
[
862+
None,
863+
]
864+
+ diag_comps,
865+
)
866+
def test_plot_customanisotropic_medium(self, eps_comp, medium_customani):
867+
"""Test plotting diagonal components of a diagonally anisotropic custom medium.
868+
diagonal components and ``None`` should succeed.
869+
"""
870+
self.make_sim(medium_customani).plot_eps(x=0, eps_component=eps_comp)
871+
872+
@pytest.mark.parametrize("eps_comp", offdiag_comps)
873+
def test_plot_customanisotropic_medium_offdiagfail(self, eps_comp, medium_customani):
874+
"""Tests that plotting off-diagonal components of a diagonally anisotropic custom medium raises an exception."""
875+
with pytest.raises(
876+
ValueError,
877+
match=f"Plotting component '{eps_comp}' of a diagonally-anisotropic permittivity tensor is not supported.",
878+
):
879+
self.make_sim(medium_customani).plot_eps(x=0, eps_component=eps_comp)
880+
881+
@pytest.mark.parametrize(
882+
"eps_comp1,eps_comp2",
883+
(
884+
pytest.param("xx", "yy", marks=pytest.mark.xfail),
885+
pytest.param("xx", "zz", marks=pytest.mark.xfail),
886+
pytest.param("yy", "zz", marks=pytest.mark.xfail),
887+
),
888+
)
889+
@check_figures_equal(extensions=("png",))
890+
def test_plot_customanisotropic_medium_diff(
891+
self, fig_test, fig_ref, eps_comp1, eps_comp2, medium_customani
892+
):
893+
"""Tests that the plots of different components of an AnisotropicMedium are actually different."""
894+
sim = self.make_sim(medium_customani)
895+
896+
ax1 = fig_test.add_subplot()
897+
sim.plot_eps(x=0, eps_component=eps_comp1, ax=ax1)
898+
ax2 = fig_ref.add_subplot()
899+
sim.plot_eps(x=0, eps_component=eps_comp2, ax=ax2)
900+
901+
696902
def test_plot():
697903
SIM_FULL.plot(x=0)
698904
plt.close()

tests/utils.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -398,6 +398,7 @@ def make_custom_data(lims, unstructured):
398398
tracer = new_box(1.0, 0, start_node)
399399
tracer_arr = new_box(np.array([[[1.0]]]), 0, start_node)
400400

401+
401402
SIM_FULL = td.Simulation(
402403
size=(8.0, 8.0, 8.0),
403404
run_time=1e-12,
@@ -470,6 +471,12 @@ def make_custom_data(lims, unstructured):
470471
geometry=td.Box(size=(1, 1, 1), center=(-1, 0, 0)),
471472
medium=td.AnisotropicMedium(xx=td.PEC, yy=td.Medium(), zz=td.Medium()),
472473
),
474+
# Test a fully anistropic medium
475+
td.Structure(
476+
geometry=td.Box(size=(1, 1, 1), center=(-1, 0, 0)),
477+
medium=td.FullyAnisotropicMedium(permittivity=[[6, 2, 3], [2, 7, 4], [3, 4, 9]]),
478+
name="fully_anisotropic_box",
479+
),
473480
td.Structure(
474481
geometry=td.GeometryGroup(geometries=[td.Box(size=(1, 1, 1), center=(-1, 0, 0))]),
475482
medium=td.PEC,

0 commit comments

Comments
 (0)