Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions tests/test_components/test_heat_charge.py
Original file line number Diff line number Diff line change
Expand Up @@ -2832,3 +2832,72 @@ def test_heat_charge_simulation_plot():
"for CHARGE simulations, resulting in at least 2 more visual elements "
"than charge_sim.scene.plot()"
)


def test_cylinder_small_radius_warning():
"""Test that warning is issued for very small cylinder radii in HeatChargeSimulation."""
solid = td.MultiPhysicsMedium(
heat=td.SolidSpec(conductivity=1, capacity=1),
name="solid",
)
background = td.MultiPhysicsMedium(
heat=td.FluidSpec(),
name="background",
)

# Test non-tapered cylinder with tiny radius
tiny_cylinder = td.Structure(
geometry=td.Cylinder(center=(0, 0, 0), radius=1e-8, length=1, axis=2),
medium=solid,
name="tiny",
)
with AssertLogLevel("WARNING", contains_str="radius"):
_ = td.HeatChargeSimulation(
center=(0, 0, 0),
size=(2, 2, 2),
medium=background,
structures=[tiny_cylinder],
grid_spec=td.UniformUnstructuredGrid(dl=0.1),
monitors=[td.TemperatureMonitor(size=(1, 1, 1), name="tmp")],
)

# Test transformed (translated) cylinder with tiny radius
tiny_cylinder_transformed = td.Structure(
geometry=td.Cylinder(center=(0, 0, 0), radius=1e-8, length=1, axis=2).translated(
x=0.1, y=0.0, z=0.0
),
medium=solid,
name="tiny_transformed",
)
with AssertLogLevel("WARNING", contains_str="radius"):
_ = td.HeatChargeSimulation(
center=(0, 0, 0),
size=(2, 2, 2),
medium=background,
structures=[tiny_cylinder_transformed],
grid_spec=td.UniformUnstructuredGrid(dl=0.1),
monitors=[td.TemperatureMonitor(size=(1, 1, 1), name="tmp")],
)

# Test tapered cylinder with steep sidewall causing negative radius_top
tapered_cylinder = td.Structure(
geometry=td.Cylinder(
center=(0, 0, 0),
radius=0.1,
length=1,
axis=2,
sidewall_angle=np.pi / 3, # 60 degrees - causes negative radius_top
reference_plane="bottom",
),
medium=solid,
name="tapered",
)
with AssertLogLevel("WARNING", contains_str="radius_top"):
_ = td.HeatChargeSimulation(
center=(0, 0, 0),
size=(2, 2, 2),
medium=background,
structures=[tapered_cylinder],
grid_spec=td.UniformUnstructuredGrid(dl=0.1),
monitors=[td.TemperatureMonitor(size=(1, 1, 1), name="tmp")],
)
5 changes: 4 additions & 1 deletion tidy3d/components/geometry/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,8 @@ def flatten_groups(
def traverse_geometries(geometry: GeometryType) -> GeometryType:
"""Iterator over all geometries within the given geometry.

Iterates over groups and clip operations within the given geometry, yielding each one.
Iterates over groups, clip operations, and transformed geometries within the given geometry,
yielding each one.

Parameters
----------
Expand All @@ -267,6 +268,8 @@ def traverse_geometries(geometry: GeometryType) -> GeometryType:
elif isinstance(geometry, base.ClipOperation):
yield from traverse_geometries(geometry.geometry_a)
yield from traverse_geometries(geometry.geometry_b)
elif isinstance(geometry, base.Transformed):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, this will have side-effects on other uses of this function too, but this addition actually seems correct here, because a Transformed geometry could itself be a group or a clip operation, and previously we were not traversing those!

yield from traverse_geometries(geometry.geometry)
yield geometry


Expand Down
46 changes: 46 additions & 0 deletions tidy3d/components/tcad/simulation/heat_charge.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
StructureStructureInterface,
)
from tidy3d.components.geometry.base import Box
from tidy3d.components.geometry.primitives import Cylinder
from tidy3d.components.geometry.utils import traverse_geometries
from tidy3d.components.material.tcad.charge import (
ChargeConductorMedium,
SemiconductorMedium,
Expand Down Expand Up @@ -127,6 +129,11 @@
# define some limits for transient heat simulations
TRANSIENT_HEAT_MAX_STEPS = 1000

# OpenCASCADE minimum tolerance for cylinder radii
OPENCASCADE_CYLINDER_RADIUS_TOL = 1e-6
# Minimum radius as fraction of the larger radius (for tapered cylinders)
MIN_CYLINDER_RADIUS_FRACTION = 0.01


class TCADAnalysisTypes(str, Enum):
"""Enumeration of the types of simulations currently supported"""
Expand Down Expand Up @@ -363,6 +370,45 @@ def check_unsupported_geometries(cls, val):
)
return val

@pd.validator("structures", always=True)
def _warn_small_cylinder_radius(cls, val):
"""Warn if any Cylinder geometry has radius too small for meshing."""
for structure in val:
for geometry in traverse_geometries(structure.geometry):
if isinstance(geometry, Cylinder):
r_bottom = geometry.radius_bottom
r_top = geometry.radius_top
is_tapered = not np.isclose(r_bottom, r_top)

# Compute minimum allowed radius (matches backend heat_mesh.py logic)
min_radius = max(
OPENCASCADE_CYLINDER_RADIUS_TOL,
MIN_CYLINDER_RADIUS_FRACTION * max(abs(r_bottom), abs(r_top)),
)

# Warn if radii are below minimum
if is_tapered:
if r_bottom < min_radius:
log.warning(
f"Cylinder 'radius_bottom' ({r_bottom:.3e}) is below the minimum "
f"radius for meshing ({min_radius:.3e}). The sidewall angle may be "
f"too steep. Will be clamped to minimum radius or mesh size, whichever is larger."
)
if r_top < min_radius:
log.warning(
f"Cylinder 'radius_top' ({r_top:.3e}) is below the minimum "
f"radius for meshing ({min_radius:.3e}). The sidewall angle may be "
f"too steep. Will be clamped to minimum radius or mesh size, whichever is larger."
)
else:
if r_bottom < min_radius:
log.warning(
f"Cylinder 'radius' ({r_bottom:.3e}) is below the minimum "
f"radius for meshing ({min_radius:.3e}). "
f"Will be clamped to minimum radius or mesh size, whichever is larger."
)
return val
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing CHANGELOG entry for user-facing feature

Low Severity

This PR adds a new user-facing feature (warning messages for small cylinder radii in HeatChargeSimulation) but does not include a corresponding entry in CHANGELOG.md. According to the repository guidelines in AGENTS.md, user-facing changes require a changelog entry under ## [Unreleased]. The new _warn_small_cylinder_radius validator will produce visible warnings to users, making this a user-facing change.

Fix in Cursor Fix in Web


@staticmethod
def _check_cross_solids(objs: tuple[Box, ...], values: dict) -> tuple[int, ...]:
"""Given model dictionary ``values``, check whether objects in list ``objs`` cross
Expand Down
Loading