Skip to content

Commit 38a6f89

Browse files
Enable PMC material
1 parent e6ed4b2 commit 38a6f89

File tree

13 files changed

+123
-10
lines changed

13 files changed

+123
-10
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

1212
- Added `eps_lim` keyword argument to `Simulation.plot_eps()` for manual control over the permittivity color limits.
1313
- Added `thickness` parameter to `LossyMetalMedium` for computing surface impedance of a thin conductor.
14+
- Added material type `PMCMedium` for perfect magnetic conductor.
1415

1516
### Changed
1617
- Relaxed bounds checking of path integrals during `WavePort` validation.

docs/api/mediums.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ Spatially uniform
1616
tidy3d.Medium
1717
tidy3d.LossyMetalMedium
1818
tidy3d.PECMedium
19+
tidy3d.PMCMedium
1920
tidy3d.FullyAnisotropicMedium
2021

2122
Spatially varying

tests/test_components/material/test_multi_physics.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ def test_delegated_attributes_work(dummy_optical):
1414

1515
# delegated names resolve
1616
assert mp.is_pec is dummy_optical.is_pec
17+
assert mp.is_pmc is dummy_optical.is_pmc
1718
assert mp._eps_plot == dummy_optical._eps_plot
1819
assert mp.viz_spec == dummy_optical.viz_spec
1920

@@ -27,6 +28,9 @@ def test_delegated_attribute_without_optical_raises():
2728
with pytest.raises(AttributeError, match=r"optical medium is 'None'"):
2829
_ = mp_no_opt.is_pec
2930

31+
with pytest.raises(AttributeError, match=r"optical medium is 'None'"):
32+
_ = mp_no_opt.is_pmc
33+
3034

3135
def test_has_cached_props(dummy_optical):
3236
mp = td.MultiPhysicsMedium(optical=dummy_optical)

tests/test_components/test_medium.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,13 @@
1414
MEDIUM = td.Medium()
1515
ANIS_MEDIUM = td.AnisotropicMedium(xx=MEDIUM, yy=MEDIUM, zz=MEDIUM)
1616
PEC = td.PECMedium()
17+
PMC = td.PMCMedium()
1718
PR = td.PoleResidue(poles=[(-1 + 1j, 2 + 2j)])
1819
SM = td.Sellmeier(coeffs=[(1, 2)])
1920
LZ = td.Lorentz(coeffs=[(1, 2, 3)])
2021
DR = td.Drude(coeffs=[(1, 2)])
2122
DB = td.Debye(coeffs=[(1, 2)])
22-
MEDIUMS = [MEDIUM, ANIS_MEDIUM, PEC, PR, SM, LZ, DR, DB]
23+
MEDIUMS = [MEDIUM, ANIS_MEDIUM, PEC, PR, SM, LZ, DR, DB, PMC]
2324

2425
f, AX = plt.subplots()
2526

@@ -141,6 +142,10 @@ def test_PEC():
141142
_ = td.Structure(geometry=td.Box(size=(1, 1, 1)), medium=td.PEC)
142143

143144

145+
def test_PMC():
146+
_ = td.Structure(geometry=td.Box(size=(1, 1, 1)), medium=td.PMC)
147+
148+
144149
def test_lossy_metal():
145150
# frequency_range shouldn't be None
146151
with pytest.raises(pydantic.ValidationError):

tests/test_components/test_time_modulation.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,10 @@ def test_unsupported_modulated_medium_types():
212212
with pytest.raises(pydantic.ValidationError):
213213
td.PECMedium(modulation_spec=modulation_spec)
214214

215+
# PMC cannot be modulated
216+
with pytest.raises(pydantic.ValidationError):
217+
td.PMCMedium(modulation_spec=modulation_spec)
218+
215219
# For Anisotropic medium, one should modulate the components, not the whole medium
216220
with pytest.raises(pydantic.ValidationError):
217221
td.AnisotropicMedium(

tests/utils.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -471,6 +471,10 @@ def make_custom_data(lims, unstructured):
471471
geometry=td.Box(size=(1, 1, 1), center=(-1, 0, 0)),
472472
medium=td.AnisotropicMedium(xx=td.PEC, yy=td.Medium(), zz=td.Medium()),
473473
),
474+
td.Structure(
475+
geometry=td.Box(size=(1, 1, 1), center=(-1, 0, 0)),
476+
medium=td.AnisotropicMedium(xx=td.PMC, yy=td.Medium(), zz=td.Medium()),
477+
),
474478
# Test a fully anistropic medium
475479
td.Structure(
476480
geometry=td.Box(size=(1, 1, 1), center=(-1, 0, 0)),

tidy3d/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,7 @@
235235
from .components.medium import (
236236
PEC,
237237
PEC2D,
238+
PMC,
238239
AbstractMedium,
239240
AnisotropicMedium,
240241
CustomAnisotropicMedium,
@@ -260,6 +261,7 @@
260261
PECMedium,
261262
PerturbationMedium,
262263
PerturbationPoleResidue,
264+
PMCMedium,
263265
PoleResidue,
264266
Sellmeier,
265267
SurfaceImpedanceFitterParam,
@@ -419,7 +421,9 @@ def set_logging_level(level: str) -> None:
419421
"PoleResidue",
420422
"AnisotropicMedium",
421423
"PEC",
424+
"PMC",
422425
"PECMedium",
426+
"PMCMedium",
423427
"Medium2D",
424428
"PEC2D",
425429
"Sellmeier",

tidy3d/components/material/multi_physics.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ def __getattr__(self, name: str):
140140

141141
DELEGATED_ATTRIBUTES = {
142142
"is_pec": self.optical,
143+
"is_pmc": self.optical,
143144
"_eps_plot": self.optical,
144145
"viz_spec": self.optical,
145146
}

tidy3d/components/medium.py

Lines changed: 69 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1355,6 +1355,11 @@ def is_pec(self):
13551355
"""Whether the medium is a PEC."""
13561356
return False
13571357

1358+
@cached_property
1359+
def is_pmc(self):
1360+
"""Whether the medium is a PMC."""
1361+
return False
1362+
13581363
def sel_inside(self, bounds: Bound) -> AbstractMedium:
13591364
"""Return a new medium that contains the minimal amount data necessary to cover
13601365
a spatial region defined by ``bounds``.
@@ -1769,6 +1774,52 @@ def is_pec(self):
17691774
PEC = PECMedium(name="PEC")
17701775

17711776

1777+
# PMC keyword
1778+
class PMCMedium(AbstractMedium):
1779+
"""Perfect magnetic conductor class.
1780+
1781+
Note
1782+
----
1783+
1784+
To avoid confusion from duplicate PMCs, must import ``tidy3d.PMC`` instance directly.
1785+
1786+
1787+
1788+
"""
1789+
1790+
@pd.validator("modulation_spec", always=True)
1791+
def _validate_modulation_spec(cls, val):
1792+
"""Check compatibility with modulation_spec."""
1793+
if val is not None:
1794+
raise ValidationError(
1795+
f"A 'modulation_spec' of class {type(val)} is not "
1796+
f"currently supported for medium class {cls}."
1797+
)
1798+
return val
1799+
1800+
@ensure_freq_in_range
1801+
def eps_model(self, frequency: float) -> complex:
1802+
# permittivity of a PMC.
1803+
return 1.0 + 0j
1804+
1805+
@cached_property
1806+
def n_cfl(self):
1807+
"""This property computes the index of refraction related to CFL condition, so that
1808+
the FDTD with this medium is stable when the time step size that doesn't take
1809+
material factor into account is multiplied by ``n_cfl``.
1810+
"""
1811+
return 1.0
1812+
1813+
@cached_property
1814+
def is_pmc(self):
1815+
"""Whether the medium is a PMC."""
1816+
return True
1817+
1818+
1819+
# PEC builtin instance
1820+
PMC = PMCMedium(name="PMC")
1821+
1822+
17721823
class Medium(AbstractMedium):
17731824
"""Dispersionless medium. Mediums define the optical properties of the materials within the simulation.
17741825
@@ -5681,6 +5732,9 @@ def plot(
56815732

56825733

56835734
IsotropicUniformMediumType = Union[
5735+
Medium, LossyMetalMedium, PoleResidue, Sellmeier, Lorentz, Debye, Drude, PECMedium, PMCMedium
5736+
]
5737+
IsotropicUniformMediumFor2DType = Union[
56845738
Medium, LossyMetalMedium, PoleResidue, Sellmeier, Lorentz, Debye, Drude, PECMedium
56855739
]
56865740
IsotropicCustomMediumType = Union[
@@ -5890,10 +5944,19 @@ def is_pec(self):
58905944
"""Whether the medium is a PEC."""
58915945
return any(self.is_comp_pec(i) for i in range(3))
58925946

5947+
@cached_property
5948+
def is_pmc(self):
5949+
"""Whether the medium is a PMC."""
5950+
return any(self.is_comp_pmc(i) for i in range(3))
5951+
58935952
def is_comp_pec(self, comp: Axis):
58945953
"""Whether the medium is a PEC."""
58955954
return isinstance(self.components[["xx", "yy", "zz"][comp]], PECMedium)
58965955

5956+
def is_comp_pmc(self, comp: Axis):
5957+
"""Whether the medium is a PMC."""
5958+
return isinstance(self.components[["xx", "yy", "zz"][comp]], PMCMedium)
5959+
58975960
def sel_inside(self, bounds: Bound):
58985961
"""Return a new medium that contains the minimal amount data necessary to cover
58995962
a spatial region defined by ``bounds``.
@@ -7007,6 +7070,7 @@ def perturbed_copy(
70077070
Medium,
70087071
AnisotropicMedium,
70097072
PECMedium,
7073+
PMCMedium,
70107074
PoleResidue,
70117075
Sellmeier,
70127076
Lorentz,
@@ -7041,7 +7105,7 @@ class Medium2D(AbstractMedium):
70417105
70427106
"""
70437107

7044-
ss: IsotropicUniformMediumType = pd.Field(
7108+
ss: IsotropicUniformMediumFor2DType = pd.Field(
70457109
...,
70467110
title="SS Component",
70477111
description="Medium describing the ss-component of the diagonal permittivity tensor. "
@@ -7052,7 +7116,7 @@ class Medium2D(AbstractMedium):
70527116
discriminator=TYPE_TAG_STR,
70537117
)
70547118

7055-
tt: IsotropicUniformMediumType = pd.Field(
7119+
tt: IsotropicUniformMediumFor2DType = pd.Field(
70567120
...,
70577121
title="TT Component",
70587122
description="Medium describing the tt-component of the diagonal permittivity tensor. "
@@ -7086,7 +7150,7 @@ def _validate_inplane_pec(cls, val, values):
70867150

70877151
@classmethod
70887152
def _weighted_avg(
7089-
cls, meds: List[IsotropicUniformMediumType], weights: List[float]
7153+
cls, meds: List[IsotropicUniformMediumFor2DType], weights: List[float]
70907154
) -> Union[PoleResidue, PECMedium]:
70917155
"""Average ``meds`` with weights ``weights``."""
70927156
eps_inf = 1
@@ -7140,7 +7204,7 @@ def volumetric_equivalent(
71407204
The 3D material corresponding to this 2D material.
71417205
"""
71427206

7143-
def get_component(med: MediumType3D, comp: Axis) -> IsotropicUniformMediumType:
7207+
def get_component(med: MediumType3D, comp: Axis) -> IsotropicUniformMediumFor2DType:
71447208
"""Extract the ``comp`` component of ``med``."""
71457209
if isinstance(med, AnisotropicMedium):
71467210
dim = "xyz"[comp]
@@ -7402,7 +7466,7 @@ def sigma_model(self, freq: float) -> complex:
74027466
return np.mean([self.ss.sigma_model(freq), self.tt.sigma_model(freq)], axis=0)
74037467

74047468
@property
7405-
def elements(self) -> Dict[str, IsotropicUniformMediumType]:
7469+
def elements(self) -> Dict[str, IsotropicUniformMediumFor2DType]:
74067470
"""The diagonal elements of the 2D medium as a dictionary."""
74077471
return dict(ss=self.ss, tt=self.tt)
74087472

tidy3d/components/mode/mode_solver.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1738,6 +1738,8 @@ def _contain_good_conductor(self) -> bool:
17381738
for medium in sim.scene.mediums:
17391739
if medium.is_pec:
17401740
return True
1741+
if medium.is_pmc:
1742+
return True
17411743
if apply_sibc and isinstance(medium, LossyMetalMedium):
17421744
return True
17431745
return False

tidy3d/components/scene.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -495,9 +495,11 @@ def _get_structure_plot_params(
495495

496496
if isinstance(medium, MultiPhysicsMedium):
497497
is_pec = medium.optical is not None and medium.optical.is_pec
498+
is_pmc = medium.optical is not None and medium.optical.is_pmc
498499
is_time_modulated = medium.optical is not None and medium.optical.is_time_modulated
499500
else:
500501
is_pec = medium.is_pec
502+
is_pmc = medium.is_pmc
501503
is_time_modulated = medium.is_time_modulated
502504

503505
if mat_index == 0 or medium == self.medium:
@@ -508,6 +510,11 @@ def _get_structure_plot_params(
508510
plot_params = plot_params.copy(
509511
update={"facecolor": "gold", "edgecolor": "k", "linewidth": 1}
510512
)
513+
elif is_pmc:
514+
# perfect magnetic conductor
515+
plot_params = plot_params.copy(
516+
update={"facecolor": "purple", "edgecolor": "k", "linewidth": 1}
517+
)
511518
elif is_time_modulated:
512519
# time modulated medium
513520
plot_params = plot_params.copy(
@@ -1259,6 +1266,11 @@ def _get_structure_eps_plot_params(
12591266
plot_params = plot_params.copy(
12601267
update={"facecolor": "gold", "edgecolor": "k", "linewidth": 1}
12611268
)
1269+
elif medium.is_pmc:
1270+
# perfect magnetic conductor
1271+
plot_params = plot_params.copy(
1272+
update={"facecolor": "purple", "edgecolor": "k", "linewidth": 1}
1273+
)
12621274
elif isinstance(medium, Medium2D):
12631275
# 2d material
12641276
plot_params = plot_params.copy(update={"edgecolor": "k", "linewidth": 1})

tidy3d/components/simulation.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3487,8 +3487,8 @@ def _warn_grid_size_too_small(cls, val, values):
34873487
freq0 = source.source_time.freq0
34883488

34893489
for medium_index, medium in enumerate(mediums):
3490-
# min wavelength in PEC is meaningless and we'll get divide by inf errors
3491-
if medium.is_pec:
3490+
# min wavelength in PEC/PMC is meaningless and we'll get divide by inf errors
3491+
if medium.is_pec or medium.is_pmc:
34923492
continue
34933493
# min wavelength in Medium2D is meaningless
34943494
if isinstance(medium, Medium2D):
@@ -3500,8 +3500,10 @@ def _warn_grid_size_too_small(cls, val, values):
35003500
for comp, (key, grid_spec) in enumerate(
35013501
zip("xyz", (val.grid_x, val.grid_y, val.grid_z))
35023502
):
3503-
if medium.is_pec or (
3504-
isinstance(medium, AnisotropicMedium) and medium.is_comp_pec(comp)
3503+
if (
3504+
medium.is_pec
3505+
or medium.is_pmc
3506+
or (isinstance(medium, AnisotropicMedium) and medium.is_comp_pec(comp))
35053507
):
35063508
n_material = 1.0
35073509
lambda_min = C_0 / freq0 / n_material

tidy3d/components/subpixel_spec.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ def courant_ratio(self) -> float:
129129

130130

131131
PECSubpixelType = Union[Staircasing, HeuristicPECStaircasing, PECConformal]
132+
PMCSubpixelType = Union[Staircasing, HeuristicPECStaircasing]
132133

133134

134135
class SurfaceImpedance(PECConformal):
@@ -176,6 +177,13 @@ class SubpixelSpec(Tidy3dBaseModel):
176177
discriminator=TYPE_TAG_STR,
177178
)
178179

180+
pmc: PMCSubpixelType = pd.Field(
181+
Staircasing(),
182+
title="Subpixel Averaging Method For PMC Interfaces",
183+
description="Subpixel averaging method applied to PMC structure interfaces.",
184+
discriminator=TYPE_TAG_STR,
185+
)
186+
179187
lossy_metal: LossyMetalSubpixelType = pd.Field(
180188
SurfaceImpedance(),
181189
title="Subpixel Averaging Method for Lossy Metal Interfaces",
@@ -190,6 +198,7 @@ def staircasing(cls) -> SubpixelSpec:
190198
dielectric=Staircasing(),
191199
metal=Staircasing(),
192200
pec=Staircasing(),
201+
pmc=Staircasing(),
193202
lossy_metal=Staircasing(),
194203
)
195204

0 commit comments

Comments
 (0)