Skip to content

Commit a59a1e1

Browse files
Enable PMC material
1 parent dac8107 commit a59a1e1

File tree

13 files changed

+133
-16
lines changed

13 files changed

+133
-16
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1515
- Added `priority` parameter to `web.run()` and related functions to allow vGPU users to set task priority (1-10) in the queue.
1616
- `EMEFieldMonitor` now supports `interval_space`.
1717
- `Simulation.precision` option allows to select `"double"` precision for very high-accuracy results. Note that this is very rarely needed, and doubles the simulation computational weight and correpsondingly FlexCredit cost.
18+
- Added material type `PMCMedium` for perfect magnetic conductor.
1819

1920
### Changed
2021
- Switched to an analytical gradient calculation for spatially-varying pole-residue models (`CustomPoleResidue`).

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
@@ -17,6 +17,7 @@ def test_delegated_attributes_work(dummy_optical):
1717

1818
# delegated names resolve
1919
assert mp.is_pec is dummy_optical.is_pec
20+
assert mp.is_pmc is dummy_optical.is_pmc
2021
assert mp._eps_plot == dummy_optical._eps_plot
2122
assert mp.viz_spec == dummy_optical.viz_spec
2223

@@ -30,6 +31,9 @@ def test_delegated_attribute_without_optical_raises():
3031
with pytest.raises(AttributeError, match=r"optical medium is 'None'"):
3132
_ = mp_no_opt.is_pec
3233

34+
with pytest.raises(AttributeError, match=r"optical medium is 'None'"):
35+
_ = mp_no_opt.is_pmc
36+
3337

3438
def test_has_cached_props(dummy_optical):
3539
mp = td.MultiPhysicsMedium(optical=dummy_optical)

tests/test_components/test_medium.py

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

2526
f, AX = plt.subplots()
2627

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

144145

146+
def test_PMC():
147+
_ = td.Structure(geometry=td.Box(size=(1, 1, 1)), medium=td.PMC)
148+
149+
145150
def test_lossy_metal():
146151
# frequency_range shouldn't be None
147152
with pytest.raises(pydantic.ValidationError):
@@ -406,6 +411,8 @@ def test_n_cfl():
406411
assert material.n_cfl == 2
407412
# PEC
408413
assert PEC.n_cfl == 1
414+
# PMC
415+
assert PMC.n_cfl == 1
409416
# anisotropic
410417
material = td.AnisotropicMedium(xx=MEDIUM, yy=td.Medium(permittivity=4), zz=MEDIUM)
411418
assert material.n_cfl == 1

tests/test_components/test_time_modulation.py

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

218+
# PMC cannot be modulated
219+
with pytest.raises(pydantic.ValidationError):
220+
td.PMCMedium(modulation_spec=modulation_spec)
221+
218222
# For Anisotropic medium, one should modulate the components, not the whole medium
219223
with pytest.raises(pydantic.ValidationError):
220224
td.AnisotropicMedium(

tests/utils.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -474,6 +474,10 @@ def make_custom_data(lims, unstructured):
474474
geometry=td.Box(size=(1, 1, 1), center=(-1, 0, 0)),
475475
medium=td.AnisotropicMedium(xx=td.PEC, yy=td.Medium(), zz=td.Medium()),
476476
),
477+
td.Structure(
478+
geometry=td.Box(size=(1, 1, 1), center=(-1, 0, 0)),
479+
medium=td.AnisotropicMedium(xx=td.PMC, yy=td.Medium(), zz=td.Medium()),
480+
),
477481
# Test a fully anistropic medium
478482
td.Structure(
479483
geometry=td.Box(size=(1, 1, 1), center=(-1, 0, 0)),
@@ -485,6 +489,10 @@ def make_custom_data(lims, unstructured):
485489
medium=td.PEC,
486490
name="pec_group",
487491
),
492+
td.Structure(
493+
geometry=td.Box(size=(1, 1, 1), center=(-1, 0, 0)),
494+
medium=td.PMC,
495+
),
488496
td.Structure(
489497
geometry=td.Cylinder(radius=1.0, length=2.0, center=(1.0, 0.0, -1.0), axis=1),
490498
medium=td.AnisotropicMedium(

tidy3d/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,7 @@
244244
from .components.medium import (
245245
PEC,
246246
PEC2D,
247+
PMC,
247248
AbstractMedium,
248249
AnisotropicMedium,
249250
CustomAnisotropicMedium,
@@ -269,6 +270,7 @@
269270
PECMedium,
270271
PerturbationMedium,
271272
PerturbationPoleResidue,
273+
PMCMedium,
272274
PoleResidue,
273275
Sellmeier,
274276
SurfaceImpedanceFitterParam,
@@ -415,6 +417,7 @@ def set_logging_level(level: str) -> None:
415417
"MU_0",
416418
"PEC",
417419
"PEC2D",
420+
"PMC",
418421
"PML",
419422
"TFSF",
420423
"Absorber",
@@ -610,6 +613,7 @@ def set_logging_level(level: str) -> None:
610613
"PECConformal",
611614
"PECMedium",
612615
"PMCBoundary",
616+
"PMCMedium",
613617
"PMLParams",
614618
"PMLTypes",
615619
"ParameterPerturbation",

tidy3d/components/material/multi_physics.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ def __getattr__(self, name: str):
141141
return None
142142

143143
DELEGATED_ATTRIBUTES = {
144+
"is_pmc": self.optical,
144145
"_eps_plot": self.optical,
145146
"viz_spec": self.optical,
146147
"eps_diagonal_numerical": self.optical,

tidy3d/components/medium.py

Lines changed: 73 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -884,7 +884,7 @@ def _validate_modulation_spec(cls, val, values):
884884
nonlinear_spec = values.get("nonlinear_spec")
885885
if val is not None and nonlinear_spec is not None:
886886
raise ValidationError(
887-
f"For medium class {cls}, 'modulation_spec' of class {type(val)} and "
887+
f"For medium class {cls.__name__}, 'modulation_spec' of class {type(val)} and "
888888
f"'nonlinear_spec' of class {type(nonlinear_spec)} are "
889889
"not simultaneously supported."
890890
)
@@ -1350,6 +1350,11 @@ def is_pec(self):
13501350
"""Whether the medium is a PEC."""
13511351
return False
13521352

1353+
@cached_property
1354+
def is_pmc(self):
1355+
"""Whether the medium is a PMC."""
1356+
return False
1357+
13531358
def sel_inside(self, bounds: Bound) -> AbstractMedium:
13541359
"""Return a new medium that contains the minimal amount data necessary to cover
13551360
a spatial region defined by ``bounds``.
@@ -1740,7 +1745,7 @@ def _validate_modulation_spec(cls, val):
17401745
if val is not None:
17411746
raise ValidationError(
17421747
f"A 'modulation_spec' of class {type(val)} is not "
1743-
f"currently supported for medium class {cls}."
1748+
f"currently supported for medium class {cls.__name__}."
17441749
)
17451750
return val
17461751

@@ -1767,6 +1772,52 @@ def is_pec(self):
17671772
PEC = PECMedium(name="PEC")
17681773

17691774

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

56455696

5646-
IsotropicUniformMediumType = Union[
5697+
IsotropicUniformMediumFor2DType = Union[
56475698
Medium, LossyMetalMedium, PoleResidue, Sellmeier, Lorentz, Debye, Drude, PECMedium
56485699
]
5700+
IsotropicUniformMediumType = Union[IsotropicUniformMediumFor2DType, PMCMedium]
56495701
IsotropicCustomMediumType = Union[
56505702
CustomPoleResidue,
56515703
CustomSellmeier,
@@ -5719,7 +5771,7 @@ def _validate_modulation_spec(cls, val):
57195771
if val is not None:
57205772
raise ValidationError(
57215773
f"A 'modulation_spec' of class {type(val)} is not "
5722-
f"currently supported for medium class {cls}. "
5774+
f"currently supported for medium class {cls.__name__}. "
57235775
"Please add modulation to each component."
57245776
)
57255777
return val
@@ -5852,10 +5904,19 @@ def is_pec(self):
58525904
"""Whether the medium is a PEC."""
58535905
return any(self.is_comp_pec(i) for i in range(3))
58545906

5907+
@cached_property
5908+
def is_pmc(self):
5909+
"""Whether the medium is a PMC."""
5910+
return any(self.is_comp_pmc(i) for i in range(3))
5911+
58555912
def is_comp_pec(self, comp: Axis):
58565913
"""Whether the medium is a PEC."""
58575914
return isinstance(self.components[["xx", "yy", "zz"][comp]], PECMedium)
58585915

5916+
def is_comp_pmc(self, comp: Axis):
5917+
"""Whether the medium is a PMC."""
5918+
return isinstance(self.components[["xx", "yy", "zz"][comp]], PMCMedium)
5919+
58595920
def sel_inside(self, bounds: Bound):
58605921
"""Return a new medium that contains the minimal amount data necessary to cover
58615922
a spatial region defined by ``bounds``.
@@ -5945,7 +6006,7 @@ def _validate_modulation_spec(cls, val):
59456006
if val is not None:
59466007
raise ValidationError(
59476008
f"A 'modulation_spec' of class {type(val)} is not "
5948-
f"currently supported for medium class {cls}."
6009+
f"currently supported for medium class {cls.__name__}."
59496010
)
59506011
return val
59516012

@@ -6970,6 +7031,7 @@ def perturbed_copy(
69707031
Medium,
69717032
AnisotropicMedium,
69727033
PECMedium,
7034+
PMCMedium,
69737035
PoleResidue,
69747036
Sellmeier,
69757037
Lorentz,
@@ -7004,7 +7066,7 @@ class Medium2D(AbstractMedium):
70047066
70057067
"""
70067068

7007-
ss: IsotropicUniformMediumType = pd.Field(
7069+
ss: IsotropicUniformMediumFor2DType = pd.Field(
70087070
...,
70097071
title="SS Component",
70107072
description="Medium describing the ss-component of the diagonal permittivity tensor. "
@@ -7015,7 +7077,7 @@ class Medium2D(AbstractMedium):
70157077
discriminator=TYPE_TAG_STR,
70167078
)
70177079

7018-
tt: IsotropicUniformMediumType = pd.Field(
7080+
tt: IsotropicUniformMediumFor2DType = pd.Field(
70197081
...,
70207082
title="TT Component",
70217083
description="Medium describing the tt-component of the diagonal permittivity tensor. "
@@ -7032,7 +7094,7 @@ def _validate_modulation_spec(cls, val):
70327094
if val is not None:
70337095
raise ValidationError(
70347096
f"A 'modulation_spec' of class {type(val)} is not "
7035-
f"currently supported for medium class {cls}."
7097+
f"currently supported for medium class {cls.__name__}."
70367098
)
70377099
return val
70387100

@@ -7049,7 +7111,7 @@ def _validate_inplane_pec(cls, val, values):
70497111

70507112
@classmethod
70517113
def _weighted_avg(
7052-
cls, meds: list[IsotropicUniformMediumType], weights: list[float]
7114+
cls, meds: list[IsotropicUniformMediumFor2DType], weights: list[float]
70537115
) -> Union[PoleResidue, PECMedium]:
70547116
"""Average ``meds`` with weights ``weights``."""
70557117
eps_inf = 1
@@ -7103,7 +7165,7 @@ def volumetric_equivalent(
71037165
The 3D material corresponding to this 2D material.
71047166
"""
71057167

7106-
def get_component(med: MediumType3D, comp: Axis) -> IsotropicUniformMediumType:
7168+
def get_component(med: MediumType3D, comp: Axis) -> IsotropicUniformMediumFor2DType:
71077169
"""Extract the ``comp`` component of ``med``."""
71087170
if isinstance(med, AnisotropicMedium):
71097171
dim = "xyz"[comp]
@@ -7365,7 +7427,7 @@ def sigma_model(self, freq: float) -> complex:
73657427
return np.mean([self.ss.sigma_model(freq), self.tt.sigma_model(freq)], axis=0)
73667428

73677429
@property
7368-
def elements(self) -> dict[str, IsotropicUniformMediumType]:
7430+
def elements(self) -> dict[str, IsotropicUniformMediumFor2DType]:
73697431
"""The diagonal elements of the 2D medium as a dictionary."""
73707432
return {"ss": self.ss, "tt": self.tt}
73717433

tidy3d/components/mode/mode_solver.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1811,6 +1811,8 @@ def _contain_good_conductor(self) -> bool:
18111811
for medium in sim.scene.mediums:
18121812
if medium.is_pec:
18131813
return True
1814+
if medium.is_pmc:
1815+
return True
18141816
if apply_sibc and isinstance(medium, LossyMetalMedium):
18151817
return True
18161818
return False

0 commit comments

Comments
 (0)