Skip to content

Commit 12b4bef

Browse files
committed
feat(FXC-5154) Warn gmsh min cylinder radii
1 parent 88e9306 commit 12b4bef

File tree

2 files changed

+92
-0
lines changed

2 files changed

+92
-0
lines changed

tests/test_components/test_heat_charge.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2789,3 +2789,54 @@ def test_heat_charge_simulation_plot():
27892789
"for CHARGE simulations, resulting in at least 2 more visual elements "
27902790
"than charge_sim.scene.plot()"
27912791
)
2792+
2793+
2794+
def test_cylinder_small_radius_warning():
2795+
"""Test that warning is issued for very small cylinder radii in HeatChargeSimulation."""
2796+
solid = td.MultiPhysicsMedium(
2797+
heat=td.SolidSpec(conductivity=1, capacity=1),
2798+
name="solid",
2799+
)
2800+
background = td.MultiPhysicsMedium(
2801+
heat=td.FluidSpec(),
2802+
name="background",
2803+
)
2804+
2805+
# Test non-tapered cylinder with tiny radius
2806+
tiny_cylinder = td.Structure(
2807+
geometry=td.Cylinder(center=(0, 0, 0), radius=1e-8, length=1, axis=2),
2808+
medium=solid,
2809+
name="tiny",
2810+
)
2811+
with AssertLogLevel("WARNING", contains_str="radius"):
2812+
_ = td.HeatChargeSimulation(
2813+
center=(0, 0, 0),
2814+
size=(2, 2, 2),
2815+
medium=background,
2816+
structures=[tiny_cylinder],
2817+
grid_spec=td.UniformUnstructuredGrid(dl=0.1),
2818+
monitors=[td.TemperatureMonitor(size=(1, 1, 1), name="tmp")],
2819+
)
2820+
2821+
# Test tapered cylinder with steep sidewall causing negative radius_top
2822+
tapered_cylinder = td.Structure(
2823+
geometry=td.Cylinder(
2824+
center=(0, 0, 0),
2825+
radius=0.1,
2826+
length=1,
2827+
axis=2,
2828+
sidewall_angle=np.pi / 3, # 60 degrees - causes negative radius_top
2829+
reference_plane="bottom",
2830+
),
2831+
medium=solid,
2832+
name="tapered",
2833+
)
2834+
with AssertLogLevel("WARNING", contains_str="radius_top"):
2835+
_ = td.HeatChargeSimulation(
2836+
center=(0, 0, 0),
2837+
size=(2, 2, 2),
2838+
medium=background,
2839+
structures=[tapered_cylinder],
2840+
grid_spec=td.UniformUnstructuredGrid(dl=0.1),
2841+
monitors=[td.TemperatureMonitor(size=(1, 1, 1), name="tmp")],
2842+
)

tidy3d/components/tcad/simulation/heat_charge.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
StructureStructureInterface,
2525
)
2626
from tidy3d.components.geometry.base import Box
27+
from tidy3d.components.geometry.primitives import Cylinder
28+
from tidy3d.components.geometry.utils import traverse_geometries
2729
from tidy3d.components.material.tcad.charge import (
2830
ChargeConductorMedium,
2931
SemiconductorMedium,
@@ -127,6 +129,9 @@
127129
# define some limits for transient heat simulations
128130
TRANSIENT_HEAT_MAX_STEPS = 1000
129131

132+
# Minimum radius for gmsh meshing (OpenCASCADE tolerance)
133+
MIN_GMSH_RADIUS = 1e-6
134+
130135

131136
class TCADAnalysisTypes(str, Enum):
132137
"""Enumeration of the types of simulations currently supported"""
@@ -363,6 +368,42 @@ def check_unsupported_geometries(cls, val):
363368
)
364369
return val
365370

371+
@pd.validator("structures", always=True)
372+
def _warn_small_cylinder_radius(cls, val):
373+
"""Warn if any Cylinder geometry has radius too small for meshing."""
374+
for structure in val:
375+
for geometry in traverse_geometries(structure.geometry):
376+
if isinstance(geometry, Cylinder):
377+
r_bottom = geometry.radius_bottom
378+
r_top = geometry.radius_top
379+
is_tapered = r_bottom != r_top
380+
381+
# Compute minimum allowed radius (matches backend heat_mesh.py logic)
382+
min_radius = max(MIN_GMSH_RADIUS, 0.01 * max(abs(r_bottom), abs(r_top)))
383+
384+
# Warn if radii are below minimum
385+
if is_tapered:
386+
if r_bottom < min_radius:
387+
log.warning(
388+
f"Cylinder 'radius_bottom' ({r_bottom:.3e}) is below the minimum "
389+
f"radius for meshing ({min_radius:.3e}). The sidewall angle may be "
390+
f"too steep. During meshing, this will be clamped to {min_radius:.3e}."
391+
)
392+
if r_top < min_radius:
393+
log.warning(
394+
f"Cylinder 'radius_top' ({r_top:.3e}) is below the minimum "
395+
f"radius for meshing ({min_radius:.3e}). The sidewall angle may be "
396+
f"too steep. During meshing, this will be clamped to {min_radius:.3e}."
397+
)
398+
else:
399+
if r_bottom < min_radius:
400+
log.warning(
401+
f"Cylinder 'radius' ({r_bottom:.3e}) is below the minimum "
402+
f"radius for meshing ({min_radius:.3e}). "
403+
f"During meshing, this will be clamped to {min_radius:.3e}."
404+
)
405+
return val
406+
366407
@staticmethod
367408
def _check_cross_solids(objs: tuple[Box, ...], values: dict) -> tuple[int, ...]:
368409
"""Given model dictionary ``values``, check whether objects in list ``objs`` cross

0 commit comments

Comments
 (0)