|
27 | 27 | HBAR,
|
28 | 28 | HERTZ,
|
29 | 29 | MICROMETER,
|
| 30 | + MU_0, |
30 | 31 | PERMITTIVITY,
|
31 | 32 | RADPERSEC,
|
32 | 33 | SECOND,
|
|
73 | 74 | from .transformation import RotationType
|
74 | 75 | from .types import (
|
75 | 76 | TYPE_TAG_STR,
|
| 77 | + ArrayComplex1D, |
76 | 78 | ArrayComplex3D,
|
77 | 79 | ArrayFloat1D,
|
78 | 80 | Ax,
|
@@ -5180,6 +5182,206 @@ class SurfaceImpedanceFitterParam(Tidy3dBaseModel):
|
5180 | 5182 | )
|
5181 | 5183 |
|
5182 | 5184 |
|
| 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 | + |
5183 | 5385 | class LossyMetalMedium(Medium):
|
5184 | 5386 | """Lossy metal that can be modeled with a surface impedance boundary condition (SIBC).
|
5185 | 5387 |
|
@@ -5210,6 +5412,14 @@ class LossyMetalMedium(Medium):
|
5210 | 5412 | 1.0, title="Permittivity", description="Relative permittivity.", units=PERMITTIVITY
|
5211 | 5413 | )
|
5212 | 5414 |
|
| 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 | + |
5213 | 5423 | frequency_range: FreqBound = pd.Field(
|
5214 | 5424 | ...,
|
5215 | 5425 | title="Frequency Range",
|
@@ -5242,8 +5452,9 @@ def _positive_conductivity(cls, val):
|
5242 | 5452 | return val
|
5243 | 5453 |
|
5244 | 5454 | @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 | + |
5247 | 5458 | omega_data = self.Hz_to_angular_freq(self.sampling_frequencies)
|
5248 | 5459 | surface_impedance = self.surface_impedance(self.sampling_frequencies)
|
5249 | 5460 | scaled_impedance = surface_impedance / (-1j * omega_data)
|
@@ -5272,19 +5483,30 @@ def scaled_surface_impedance_model(self) -> PoleResidue:
|
5272 | 5483 |
|
5273 | 5484 | res_inf /= scaling_factor
|
5274 | 5485 | residues /= scaling_factor
|
| 5486 | + return PoleResidue(eps_inf=res_inf, poles=list(zip(poles, residues))), error |
5275 | 5487 |
|
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] |
5277 | 5492 |
|
5278 | 5493 | @cached_property
|
5279 | 5494 | def num_poles(self) -> int:
|
5280 | 5495 | """Number of poles in the fitted model."""
|
5281 | 5496 | return len(self.scaled_surface_impedance_model.poles)
|
5282 | 5497 |
|
5283 | 5498 | def surface_impedance(self, frequencies: ArrayFloat1D):
|
5284 |
| - """Computing surface impedance.""" |
| 5499 | + """Computing surface impedance including surface roughness effects.""" |
5285 | 5500 | # compute complex-valued skin depth
|
5286 | 5501 | 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) |
5288 | 5510 |
|
5289 | 5511 | @cached_property
|
5290 | 5512 | def sampling_frequencies(self) -> ArrayFloat1D:
|
|
0 commit comments