Skip to content

Commit be3bdbc

Browse files
Add metal surface roughness models
1 parent c0276e7 commit be3bdbc

File tree

5 files changed

+264
-7
lines changed

5 files changed

+264
-7
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
### Added
1111
- New `LobeMeasurer` tool in the `microwave` plugin that locates lobes in antenna patterns and calculates lobe measures like half-power beamwidth and sidelobe level.
1212
- Validation step that raises a `ValueError` when no frequency-domain monitors are present, preventing invalid adjoint runs.
13+
- Metal surface roughness models: modified Hammerstad, Huray Snowball, and Cannonball-Huray.
1314

1415
### Changed
1516

docs/api/mediums.rst

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,16 @@ Spatially varying
2626

2727
tidy3d.CustomMedium
2828

29-
Fitting parameters
30-
^^^^^^^^^^^^^^^^^^
29+
Lossy Metal parameters
30+
^^^^^^^^^^^^^^^^^^^^^^
3131

3232
.. autosummary::
3333
:toctree: _autosummary/
3434
:template: module.rst
3535

3636
tidy3d.SurfaceImpedanceFitterParam
37+
tidy3d.HammerstadSurfaceRoughness
38+
tidy3d.HuraySurfaceRoughness
3739

3840
Dispersive Mediums
3941
------------------

tests/test_components/test_medium.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,34 @@ def test_lossy_metal():
175175
num_poles = mat.num_poles
176176

177177

178+
def test_lossy_metal_surface_roughness():
179+
mat_orig = td.LossyMetalMedium(
180+
conductivity=41.0,
181+
frequency_range=(1e9, 10e9),
182+
)
183+
skin_depth = 1.1
184+
frequency = 1e12
185+
186+
# Hammerstad
187+
rq = 0.5
188+
mat = mat_orig.updated_copy(roughness=td.HammerstadSurfaceRoughness(rq=rq))
189+
# verify power loss correction factor compared to analytical formula
190+
complex_factor = mat.roughness.roughness_correction_factor(frequency, skin_depth)
191+
power_factor = 1 + 2 / np.pi * np.arctan(1.4 * (rq / skin_depth) ** 2)
192+
assert np.isclose(np.real(complex_factor) + np.imag(complex_factor), power_factor)
193+
_, residue = mat._fitting_result
194+
assert residue < 1e-2 # small enough residue indicating causality
195+
196+
# Huray
197+
mat = mat_orig.updated_copy(roughness=td.HuraySurfaceRoughness.from_cannonball_huray(rq))
198+
# verify power loss correction factor compared to analytical formula
199+
complex_factor = mat.roughness.roughness_correction_factor(frequency, skin_depth)
200+
power_factor = 1 + 7 / 3 * np.pi / (1 + skin_depth / rq + (skin_depth / rq) ** 2 / 2)
201+
assert np.isclose(np.real(complex_factor) + np.imag(complex_factor), power_factor)
202+
_, residue = mat._fitting_result
203+
assert residue < 1e-2 # small enough residue indicates causality
204+
205+
178206
def test_medium_dispersion():
179207
# construct media
180208
m_PR = td.PoleResidue(eps_inf=1.0, poles=[((-1 + 2j), (1 + 3j)), ((-2 + 4j), (1 + 5j))])

tidy3d/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,8 @@
240240
Debye,
241241
Drude,
242242
FullyAnisotropicMedium,
243+
HammerstadSurfaceRoughness,
244+
HuraySurfaceRoughness,
243245
KerrNonlinearity,
244246
Lorentz,
245247
LossyMetalMedium,
@@ -424,6 +426,8 @@ def set_logging_level(level: str) -> None:
424426
"CustomAnisotropicMedium",
425427
"LossyMetalMedium",
426428
"SurfaceImpedanceFitterParam",
429+
"HammerstadSurfaceRoughness",
430+
"HuraySurfaceRoughness",
427431
"RotationAroundAxis",
428432
"PerturbationMedium",
429433
"PerturbationPoleResidue",

tidy3d/components/medium.py

Lines changed: 227 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
HBAR,
2828
HERTZ,
2929
MICROMETER,
30+
MU_0,
3031
PERMITTIVITY,
3132
RADPERSEC,
3233
SECOND,
@@ -73,6 +74,7 @@
7374
from .transformation import RotationType
7475
from .types import (
7576
TYPE_TAG_STR,
77+
ArrayComplex1D,
7678
ArrayComplex3D,
7779
ArrayFloat1D,
7880
Ax,
@@ -5180,6 +5182,206 @@ class SurfaceImpedanceFitterParam(Tidy3dBaseModel):
51805182
)
51815183

51825184

5185+
class AbstractSurfaceRoughness(Tidy3dBaseModel):
5186+
"""Abstract class for modeling surface roughness of lossy metal."""
5187+
5188+
@abstractmethod
5189+
def roughness_correction_factor(
5190+
self, frequency: ArrayFloat1D, skin_depths: ArrayFloat1D
5191+
) -> ArrayComplex1D:
5192+
"""Complex-valued roughness correction factor applied to surface impedance.
5193+
5194+
Notes
5195+
-----
5196+
The roughness correction factor should be causal. It is multiplied to the
5197+
surface impedance of the lossy metal to account for the effects of surface roughness.
5198+
5199+
Parameters
5200+
----------
5201+
frequency : ArrayFloat1D
5202+
Frequency to evaluate roughness correction factor at (Hz).
5203+
skin_depths : ArrayFloat1D
5204+
Skin depths of the lossy metal that is frequency-dependent.
5205+
5206+
Returns
5207+
-------
5208+
ArrayComplex1D
5209+
The causal roughness correction factor evaluated at ``frequency``.
5210+
"""
5211+
5212+
5213+
class HammerstadSurfaceRoughness(AbstractSurfaceRoughness):
5214+
"""Modified Hammerstad surface roughness model. It's a popular model that works well
5215+
under 5 GHz for surface roughness below 2 micrometer RMS.
5216+
5217+
Note
5218+
----
5219+
5220+
The power loss compared to smooth surface is described by:
5221+
5222+
.. math::
5223+
5224+
1 + (RF-1) \\frac{2}{\\pi}\\arctan(1.4\\frac{R_q^2}{\\delta^2})
5225+
5226+
where :math:`\\delta` is skin depth, :math:`R_q` the RMS peak-to-vally height, and RF
5227+
roughness factor.
5228+
5229+
Note
5230+
----
5231+
This model is based on:
5232+
5233+
Y. Shlepnev, C. Nwachukwu, "Roughness characterization for interconnect analysis",
5234+
2011 IEEE International Symposium on Electromagnetic Compatibility,
5235+
(DOI: 10.1109/ISEMC.2011.6038367), 2011.
5236+
5237+
V. Dmitriev-Zdorov, B. Simonovich, I. Kochikov, "A Causal Conductor Roughness Model
5238+
and its Effect on Transmission Line Characteristics", Signal Integrity Journal, 2018.
5239+
"""
5240+
5241+
rq: pd.PositiveFloat = pd.Field(
5242+
...,
5243+
title="RMS Peak-to-Valley Height",
5244+
description="RMS peak-to-valley height (Rq) of the surface roughness.",
5245+
units=MICROMETER,
5246+
)
5247+
5248+
roughness_factor: float = pd.Field(
5249+
2.0,
5250+
title="Roughness Factor",
5251+
description="Expected maximal increase in conductor losses due to roughness effect. "
5252+
"Value 2 gives the classic Hammerstad equation.",
5253+
gt=1.0,
5254+
)
5255+
5256+
def roughness_correction_factor(
5257+
self, frequency: ArrayFloat1D, skin_depths: ArrayFloat1D
5258+
) -> ArrayComplex1D:
5259+
"""Complex-valued roughness correction factor applied to surface impedance.
5260+
5261+
Notes
5262+
-----
5263+
The roughness correction factor should be causal. It is multiplied to the
5264+
surface impedance of the lossy metal to account for the effects of surface roughness.
5265+
5266+
Parameters
5267+
----------
5268+
frequency : ArrayFloat1D
5269+
Frequency to evaluate roughness correction factor at (Hz).
5270+
skin_depths : ArrayFloat1D
5271+
Skin depths of the lossy metal that is frequency-dependent.
5272+
5273+
Returns
5274+
-------
5275+
ArrayComplex1D
5276+
The causal roughness correction factor evaluated at ``frequency``.
5277+
"""
5278+
normalized_laplace = -1.4j * (self.rq / skin_depths) ** 2
5279+
sqrt_normalized_laplace = np.sqrt(normalized_laplace)
5280+
causal_response = np.log(
5281+
1 + 2 * sqrt_normalized_laplace / (1 + normalized_laplace)
5282+
) + 2 * np.arctan(sqrt_normalized_laplace)
5283+
return 1 + (self.roughness_factor - 1) / np.pi * causal_response
5284+
5285+
5286+
class HuraySurfaceRoughness(AbstractSurfaceRoughness):
5287+
"""Huray surface roughness model.
5288+
5289+
Note
5290+
----
5291+
5292+
The power loss compared to smooth surface is described by:
5293+
5294+
.. math::
5295+
5296+
\\frac{A_{matte}}{A_{flat}} + \\frac{3}{2}\\sum_i f_i/[1+\\frac{\\delta}{r_i}+\\frac{\\delta^2}{2r_i^2}]
5297+
5298+
where :math:`\\delta` is skin depth, :math:`r_i` the radius of sphere,
5299+
:math:`\\frac{A_{matte}}{A_{flat}}` the relative area of the matte compared to flat surface,
5300+
and :math:`f_i=N_i4\\pi r_i^2/A_{flat}` the ratio of total sphere
5301+
surface area (number of spheres :math:`N_i` times the individual sphere surface area)
5302+
to the flat surface area.
5303+
5304+
Note
5305+
----
5306+
This model is based on:
5307+
5308+
J. Eric Bracken, "A Causal Huray Model for Surface Roughness", DesignCon, 2012.
5309+
"""
5310+
5311+
relative_area: pd.PositiveFloat = pd.Field(
5312+
1,
5313+
title="Relative Area",
5314+
description="Relative area of the matte base compared to a flat surface",
5315+
)
5316+
5317+
coeffs: Tuple[Tuple[pd.PositiveFloat, pd.PositiveFloat], ...] = pd.Field(
5318+
...,
5319+
title="Coefficients for surface ratio and sphere radius",
5320+
description="List of (:math:`f_i, r_i`) values for model, where :math:`f_i` is "
5321+
"the ratio of total sphere surface area to the flat surface area, and :math:`r_i` "
5322+
"the radius of the sphere.",
5323+
units=(None, MICROMETER),
5324+
)
5325+
5326+
@classmethod
5327+
def from_cannonball_huray(cls, radius: float) -> HuraySurfaceRoughness:
5328+
"""Construct a Cannonball-Huray model.
5329+
5330+
Note
5331+
----
5332+
5333+
The power loss compared to smooth surface is described by:
5334+
5335+
.. math::
5336+
5337+
1 + \\frac{7\\pi}{3} \\frac{1}{1+\\frac{\\delta}{r}+\\frac{\\delta^2}{2r^2}}
5338+
5339+
Parameters
5340+
----------
5341+
radius : float
5342+
Radius of the sphere.
5343+
5344+
Returns
5345+
-------
5346+
HuraySurfaceRoughness
5347+
The Huray surface roughness model.
5348+
"""
5349+
return cls(relative_area=1, coeffs=[(14.0 / 9 * np.pi, radius)])
5350+
5351+
def roughness_correction_factor(
5352+
self, frequency: ArrayFloat1D, skin_depths: ArrayFloat1D
5353+
) -> ArrayComplex1D:
5354+
"""Complex-valued roughness correction factor applied to surface impedance.
5355+
5356+
Notes
5357+
-----
5358+
The roughness correction factor should be causal. It is multiplied to the
5359+
surface impedance of the lossy metal to account for the effects of surface roughness.
5360+
5361+
Parameters
5362+
----------
5363+
frequency : ArrayFloat1D
5364+
Frequency to evaluate roughness correction factor at (Hz).
5365+
skin_depths : ArrayFloat1D
5366+
Skin depths of the lossy metal that is frequency-dependent.
5367+
5368+
Returns
5369+
-------
5370+
ArrayComplex1D
5371+
The causal roughness correction factor evaluated at ``frequency``.
5372+
"""
5373+
5374+
correction = self.relative_area
5375+
for f, r in self.coeffs:
5376+
normalized_laplace = -2j * (r / skin_depths) ** 2
5377+
sqrt_normalized_laplace = np.sqrt(normalized_laplace)
5378+
correction += 1.5 * f / (1 + 1 / sqrt_normalized_laplace)
5379+
return correction
5380+
5381+
5382+
SurfaceRoughnessType = Union[HammerstadSurfaceRoughness, HuraySurfaceRoughness]
5383+
5384+
51835385
class LossyMetalMedium(Medium):
51845386
"""Lossy metal that can be modeled with a surface impedance boundary condition (SIBC).
51855387
@@ -5210,6 +5412,14 @@ class LossyMetalMedium(Medium):
52105412
1.0, title="Permittivity", description="Relative permittivity.", units=PERMITTIVITY
52115413
)
52125414

5415+
roughness: SurfaceRoughnessType = pd.Field(
5416+
None,
5417+
title="Surface Roughness Model",
5418+
description="Surface roughness model that applies a frequency-dependent scaling "
5419+
"factor to surface impedance.",
5420+
discriminator=TYPE_TAG_STR,
5421+
)
5422+
52135423
frequency_range: FreqBound = pd.Field(
52145424
...,
52155425
title="Frequency Range",
@@ -5242,8 +5452,9 @@ def _positive_conductivity(cls, val):
52425452
return val
52435453

52445454
@cached_property
5245-
def scaled_surface_impedance_model(self) -> PoleResidue:
5246-
"""Fitted surface impedance divided by (-j \\omega) using pole-residue pair model within ``frequency_range``."""
5455+
def _fitting_result(self) -> Tuple[PoleResidue, float]:
5456+
"""Fitted scaled surface impedance and residue."""
5457+
52475458
omega_data = self.Hz_to_angular_freq(self.sampling_frequencies)
52485459
surface_impedance = self.surface_impedance(self.sampling_frequencies)
52495460
scaled_impedance = surface_impedance / (-1j * omega_data)
@@ -5272,19 +5483,30 @@ def scaled_surface_impedance_model(self) -> PoleResidue:
52725483

52735484
res_inf /= scaling_factor
52745485
residues /= scaling_factor
5486+
return PoleResidue(eps_inf=res_inf, poles=list(zip(poles, residues))), error
52755487

5276-
return PoleResidue(eps_inf=res_inf, poles=list(zip(poles, residues)))
5488+
@cached_property
5489+
def scaled_surface_impedance_model(self) -> PoleResidue:
5490+
"""Fitted surface impedance divided by (-j \\omega) using pole-residue pair model within ``frequency_range``."""
5491+
return self._fitting_result[0]
52775492

52785493
@cached_property
52795494
def num_poles(self) -> int:
52805495
"""Number of poles in the fitted model."""
52815496
return len(self.scaled_surface_impedance_model.poles)
52825497

52835498
def surface_impedance(self, frequencies: ArrayFloat1D):
5284-
"""Computing surface impedance."""
5499+
"""Computing surface impedance including surface roughness effects."""
52855500
# compute complex-valued skin depth
52865501
n, k = self.nk_model(frequencies)
5287-
return ETA_0 / (n + 1j * k)
5502+
5503+
# with surface roughness effects
5504+
correction = 1.0
5505+
if self.roughness is not None:
5506+
skin_depths = 1 / np.sqrt(np.pi * frequencies * MU_0 * self.conductivity)
5507+
correction = self.roughness.roughness_correction_factor(frequencies, skin_depths)
5508+
5509+
return correction * ETA_0 / (n + 1j * k)
52885510

52895511
@cached_property
52905512
def sampling_frequencies(self) -> ArrayFloat1D:

0 commit comments

Comments
 (0)