diff --git a/tidy3d/__init__.py b/tidy3d/__init__.py index a3993e3863..a7ec9f24db 100644 --- a/tidy3d/__init__.py +++ b/tidy3d/__init__.py @@ -24,6 +24,7 @@ from tidy3d.components.spice.sources.dc import DCCurrentSource, DCVoltageSource from tidy3d.components.spice.sources.types import VoltageSourceType from tidy3d.components.tcad.analysis.heat_simulation_type import UnsteadyHeatAnalysis, UnsteadySpec +from tidy3d.components.tcad.boundary.heat import VerticalNaturalConvectionCoeffModel from tidy3d.components.tcad.boundary.specification import ( HeatBoundarySpec, HeatChargeBoundarySpec, @@ -693,6 +694,7 @@ def set_logging_level(level: str) -> None: "UnsteadyHeatAnalysis", "UnsteadySpec", "Updater", + "VerticalNaturalConvectionCoeffModel", "VisualizationSpec", "VoltageBC", "VoltageSourceType", diff --git a/tidy3d/components/material/tcad/heat.py b/tidy3d/components/material/tcad/heat.py index a33925f210..42f5fbf3a6 100644 --- a/tidy3d/components/material/tcad/heat.py +++ b/tidy3d/components/material/tcad/heat.py @@ -10,8 +10,11 @@ from tidy3d.components.base import Tidy3dBaseModel from tidy3d.constants import ( DENSITY, + DYNAMIC_VISCOSITY, + SPECIFIC_HEAT, SPECIFIC_HEAT_CAPACITY, THERMAL_CONDUCTIVITY, + THERMAL_EXPANSIVITY, ) @@ -46,11 +49,100 @@ class FluidMedium(AbstractHeatMedium): """Fluid medium. Heat simulations will not solve for temperature in a structure that has a medium with this 'heat_spec'. - Example - ------- - >>> solid = FluidMedium() + The full set of parameters is primarily intended for calculations involving natural + convection, where they are used to determine the heat transfer coefficient. + In the current version, these specific properties may not be utilized for + other boundary condition types. + + Attributes + ---------- + thermal_conductivity : float, optional + Thermal conductivity ($k$) of the fluid in $W/(\\mu m \\cdot K)$. + viscosity : float, optional + Dynamic viscosity ($\\mu$) of the fluid in $kg/(\\mu m \\cdot s)$. + specific_heat : float, optional + Specific heat ($c_p$) of the fluid in $\\mu m^2/(s^2 \\cdot K)$. + density : float, optional + Density ($\rho$) of the fluid in $kg/\\mu m^3$. + expansivity : float, optional + Thermal expansion coefficient ($\beta$) of the fluid in $1/K$. + + Examples + -------- + >>> # It is most convenient to define the fluid from standard SI units + >>> # using the `from_si_units` classmethod. + >>> # The following defines air at approximately 20°C. + >>> air_from_si = FluidMedium.from_si_units( + ... thermal_conductivity=0.0257, # Unit: W/(m*K) + ... viscosity=1.81e-5, # Unit: Pa*s + ... specific_heat=1005, # Unit: J/(kg*K) + ... density=1.204, # Unit: kg/m^3 + ... expansivity=1/293.15 # Unit: 1/K + ... ) + + >>> # One can also define the medium directly in Tidy3D units. + >>> # The following is equivalent to the example above. + >>> air_direct = FluidMedium( + ... thermal_conductivity=2.57e-8, + ... viscosity=1.81e-11, + ... specific_heat=1.005e+15, + ... density=1.204e-18, + ... expansivity=0.00341 + ... ) """ + thermal_conductivity: pd.NonNegativeFloat = pd.Field( + default=None, + title="Fluid Thermal Conductivity", + description="Thermal conductivity (k) of the fluid.", + units=THERMAL_CONDUCTIVITY, + ) + viscosity: pd.NonNegativeFloat = pd.Field( + default=None, + title="Fluid Dynamic Viscosity", + description="Dynamic viscosity (μ) of the fluid.", + units=DYNAMIC_VISCOSITY, + ) + specific_heat: pd.NonNegativeFloat = pd.Field( + default=None, + title="Fluid Specific Heat", + description="Specific heat of the fluid at constant pressure.", + units=SPECIFIC_HEAT, + ) + density: pd.NonNegativeFloat = pd.Field( + default=None, + title="Fluid Density", + description="Density (ρ) of the fluid.", + units=DENSITY, + ) + expansivity: pd.NonNegativeFloat = pd.Field( + default=None, + title="Fluid Thermal Expansivity", + description="Thermal expansion coefficient (β) of the fluid.", + units=THERMAL_EXPANSIVITY, + ) + + def from_si_units( + thermal_conductivity: pd.NonNegativeFloat, + viscosity: pd.NonNegativeFloat, + specific_heat: pd.NonNegativeFloat, + density: pd.NonNegativeFloat, + expansivity: pd.NonNegativeFloat, + ): + thermal_conductivity_tidy = thermal_conductivity / 1e6 # W/(m*K) -> W/(um*K) + viscosity_tidy = viscosity / 1e6 # Pa*s -> kg/(um*s) + specific_heat_tidy = specific_heat * 1e12 # J/(kg*K) -> um**2/(s**2*K) + density_tidy = density / 1e18 # kg/m**3 -> kg/um**3 + expansivity_tidy = expansivity # 1/K -> 1/K (no change) + + return FluidMedium( + thermal_conductivity=thermal_conductivity_tidy, + viscosity=viscosity_tidy, + specific_heat=specific_heat_tidy, + density=density_tidy, + expansivity=expansivity_tidy, + ) + class FluidSpec(FluidMedium): """Fluid medium class for backwards compatibility""" diff --git a/tidy3d/components/tcad/boundary/heat.py b/tidy3d/components/tcad/boundary/heat.py index 9671dee6a9..59337a9998 100644 --- a/tidy3d/components/tcad/boundary/heat.py +++ b/tidy3d/components/tcad/boundary/heat.py @@ -2,10 +2,21 @@ from __future__ import annotations +from typing import Union + import pydantic.v1 as pd +from tidy3d.components.base import Tidy3dBaseModel +from tidy3d.components.material.tcad.heat import FluidMedium from tidy3d.components.tcad.boundary.abstract import HeatChargeBC -from tidy3d.constants import HEAT_FLUX, HEAT_TRANSFER_COEFF, KELVIN +from tidy3d.constants import ( + ACCELERATION, + GRAV_ACC, + HEAT_FLUX, + HEAT_TRANSFER_COEFF, + KELVIN, + MICROMETER, +) class TemperatureBC(HeatChargeBC): @@ -40,6 +51,64 @@ class HeatFluxBC(HeatChargeBC): ) +class VerticalNaturalConvectionCoeffModel(Tidy3dBaseModel): + """ + Specification for natural convection from a vertical plate. + + This class calculates the heat transfer coefficient (h) based on fluid + properties and an expected temperature difference, then provides these + values as 'base' and 'exponent' for a generalized heat flux equation + q = base * (T_surf - T_fluid)^exponent + base * (T_surf - T_fluid). + """ + + medium: FluidMedium = pd.Field( + default=None, + title="Interface medium", + description="Medium to use for heat transfer coefficient.", + ) + + plate_length: pd.NonNegativeFloat = pd.Field( + title="Plate Characteristic Length", + description="Characteristic length (L), defined as the height of the vertical plate.", + units=MICROMETER, + ) + + gravity: pd.NonNegativeFloat = pd.Field( + default=GRAV_ACC, + title="Gravitational Acceleration", + description="Gravitational acceleration (g).", + units=ACCELERATION, + ) + + def from_si_units( + plate_length: pd.NonNegativeFloat, + medium: FluidMedium = None, + gravity: pd.NonNegativeFloat = GRAV_ACC * 1e-6, + ): + """ + Create an instance from standard SI units. + + Args: + plate_length: Plate characteristic length in [m]. + gravity: Gravitational acceleration in [m/s**2]. + + Returns: + An instance of VerticalNaturalConvectionCoeffModel with all values + converted to Tidy3D's internal unit system. + """ + + # --- Apply conversion factors --- + # value_tidy = value_si * factor + plate_length_tidy = plate_length * 1e6 # m -> um + g_tidy = gravity * 1e6 # m/s**2 -> um/s**2 + + return VerticalNaturalConvectionCoeffModel( + medium=medium, + plate_length=plate_length_tidy, + gravity=g_tidy, + ) + + class ConvectionBC(HeatChargeBC): """Convective thermal boundary conditions. @@ -55,7 +124,7 @@ class ConvectionBC(HeatChargeBC): units=KELVIN, ) - transfer_coeff: pd.NonNegativeFloat = pd.Field( + transfer_coeff: Union[pd.NonNegativeFloat, VerticalNaturalConvectionCoeffModel] = pd.Field( title="Heat Transfer Coefficient", description=f"Heat flux value in units of {HEAT_TRANSFER_COEFF}.", units=HEAT_TRANSFER_COEFF, diff --git a/tidy3d/components/tcad/simulation/heat_charge.py b/tidy3d/components/tcad/simulation/heat_charge.py index 7b4b693c74..f68975de10 100644 --- a/tidy3d/components/tcad/simulation/heat_charge.py +++ b/tidy3d/components/tcad/simulation/heat_charge.py @@ -9,6 +9,8 @@ import numpy as np import pydantic.v1 as pd +from tidy3d import FluidMedium, VerticalNaturalConvectionCoeffModel + try: from matplotlib import colormaps except ImportError: @@ -455,6 +457,89 @@ def check_voltage_array_if_capacitance(cls, values): ) return values + @pd.root_validator(skip_on_failure=True) + def check_natural_convection_bc(cls, values): + """Make sure that natural convection BCs are defined correctly.""" + boundary_spec = values.get("boundary_spec") + if not boundary_spec: + return values + + structures = values["structures"] + boundary_spec = values["boundary_spec"] + bg_medium = values["medium"] + + # Create mappings for easy lookup of media and structures by name. + media = {s.medium.name: s.medium for s in structures if s.medium.name} + if bg_medium and bg_medium.name: + media[bg_medium.name] = bg_medium + structures_map = {s.name: s for s in structures if s.name} + + def check_fluid_medium_attr(fluid_medium): + if ( + (fluid_medium.thermal_conductivity is None) + or (fluid_medium.viscosity is None) + or (fluid_medium.specific_heat is None) + or (fluid_medium.density is None) + or (fluid_medium.expansivity is None) + ): + raise SetupError( + f"Boundary spec at index {i}: The fluid medium at the natural convection interface " + f"must have 'thermal_conductivity', 'viscosity', 'specific_heat', 'density' and 'expansivity' defined." + ) + + for i, bc in enumerate(boundary_spec): + if not ( + isinstance(bc.condition, ConvectionBC) + and isinstance(bc.condition.transfer_coeff, VerticalNaturalConvectionCoeffModel) + ): + continue + + natural_conv_model = bc.condition.transfer_coeff + placement = bc.placement + + # Case 1: The fluid medium is inferred from the placement interface. + # We use direct dictionary access, assuming 'names_exist_bcs' validator has already run. + if natural_conv_model.medium is None: + if isinstance(placement, MediumMediumInterface): + med1 = media[placement.mediums[0]] + med2 = media[placement.mediums[1]] + elif isinstance(placement, StructureStructureInterface): + med1 = structures_map[placement.structures[0]].medium + med2 = structures_map[placement.structures[1]].medium + else: + raise SetupError( + f"Boundary spec at index {i}: 'VerticalNaturalConvectionCoeffModel' with no medium specified requires " + f"the 'placement' to be of type 'MediumMediumInterface' or 'StructureStructureInterface', " + f"but got '{type(placement).__name__}'." + ) + specs = [ + med1.heat if isinstance(med1, MultiPhysicsMedium) else med1, + med2.heat if isinstance(med2, MultiPhysicsMedium) else med2, + ] + + # Check for a single fluid in the interface. + is_fluid = [isinstance(s, FluidMedium) for s in specs] + if is_fluid.count(True) != 1: + raise SetupError( + f"Boundary spec at index {i}: A natural convection boundary at an interface " + f"must be between exactly one solid and one fluid medium. " + f"Found types '{type(specs[0]).__name__}' and '{type(specs[1]).__name__}'." + ) + fluid_medium = specs[is_fluid.index(True)] + check_fluid_medium_attr(fluid_medium) + + # Case 2: The fluid medium IS specified directly in the convection model. + else: + fluid_medium = natural_conv_model.medium + if not isinstance(fluid_medium, FluidMedium): + raise SetupError( + f"Boundary spec at index {i}: The medium '{fluid_medium.name}' specified in " + f"'VerticalNaturalConvectionCoeffModel' must be a fluid, but it has a heat " + f"spec of type '{type(fluid_medium).__name__}'." + ) + check_fluid_medium_attr(fluid_medium) + return values + @pd.validator("size", always=True) def check_zero_dim_domain(cls, val, values): """Error if heat domain have zero dimensions.""" diff --git a/tidy3d/components/tcad/types.py b/tidy3d/components/tcad/types.py index dec3f44f62..5926eba088 100644 --- a/tidy3d/components/tcad/types.py +++ b/tidy3d/components/tcad/types.py @@ -4,7 +4,11 @@ from tidy3d.components.tcad.bandgap import SlotboomBandGapNarrowing from tidy3d.components.tcad.boundary.charge import CurrentBC, InsulatingBC, VoltageBC -from tidy3d.components.tcad.boundary.heat import ConvectionBC, HeatFluxBC, TemperatureBC +from tidy3d.components.tcad.boundary.heat import ( + ConvectionBC, + HeatFluxBC, + TemperatureBC, +) from tidy3d.components.tcad.generation_recombination import ( AugerRecombination, RadiativeRecombination, diff --git a/tidy3d/constants.py b/tidy3d/constants.py index 7edcf22278..a9a0e3808d 100644 --- a/tidy3d/constants.py +++ b/tidy3d/constants.py @@ -53,6 +53,11 @@ Boltzmann constant [eV/K] """ +GRAV_ACC = 9.80665 * 1e6 +""" +Gravitational acceleration (g) [um/s^2].", +""" + # floating point precisions dp_eps = np.finfo(np.float64).eps """ @@ -221,6 +226,26 @@ Amperes per square micrometer """ +DYNAMIC_VISCOSITY = "kg/(um*s)" +""" +Kilograms per (micrometer second) +""" + +SPECIFIC_HEAT = "um^2/(s^2*K)" +""" +Square micrometers per (square second Kelvin). +""" + +THERMAL_EXPANSIVITY = "1/K" +""" +Inverse Kelvin. +""" + +ACCELERATION = "um/s^2" +""" +Acceleration unit. +""" + LARGE_NUMBER = 1e10 """ Large number used for comparing infinity.