Skip to content

Change coordinates when Charge simulations aren't in the xy plane #2610

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 5 commits into from
Closed
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
115 changes: 115 additions & 0 deletions tests/test_components/test_heat_charge.py
Original file line number Diff line number Diff line change
Expand Up @@ -1336,6 +1336,14 @@ def test_charge_simulation(
)
_ = sim.updated_copy(boundary_spec=[new_bc_p, bc_n])

# test error is raised with 1D monitors
with pytest.raises(pd.ValidationError):
_ = sim.updated_copy(
monitors=[
charge_global_mnt.updated_copy(size=(1, 0, 0)),
]
)

def test_doping_distributions(self):
"""Test doping distributions."""
# Implementation needed
Expand Down Expand Up @@ -2039,3 +2047,110 @@ def test_heat_conduction_simulations():
with pytest.raises(pd.ValidationError):
# This should error since the conduction simulation doesn't have a monitor
_ = sim.updated_copy(monitors=[temp_monitor])


def test_charge_copy_xy():
"""Test weather the function _create_charge_copy_xy works as expected"""
# create a Gaussian doping box
d_box = td.GaussianDoping(
center=(0, 0, 0),
size=(1, 1, 2),
ref_con=1e15,
concentration=1e18,
width=0.1,
source="zmin",
)

# create SpatialDataArray doping
doping_da = td.SpatialDataArray(
data=np.random.random((3, 1, 3)),
coords={"x": [0, 1, 2], "y": [0], "z": [0, 1, 2]},
name="doping_datarray",
)

# create a semiconductor medium
semiconductor = td.SemiconductorMedium(
permittivity=11.7,
N_c=1e18,
N_v=1e18,
E_g=1.12,
mobility_n=td.ConstantMobilityModel(mu=1500),
mobility_p=td.ConstantMobilityModel(mu=500),
N_d=doping_da,
N_a=[d_box, d_box],
)

# create a structure with the semiconductor medium
structure = td.Structure(
geometry=td.Box(center=(0, 0, 0), size=(1, 2, 3)),
medium=td.MultiPhysicsMedium(charge=semiconductor),
name="test_structure",
)

structure2 = td.Structure(
geometry=td.Box(center=(0, 0, 0), size=(1, 2, 3)),
medium=td.MultiPhysicsMedium(charge=td.ChargeInsulatorMedium(permittivity=4.0)),
name="test_structure2",
)

# create boundary conditions
bc1 = td.HeatChargeBoundarySpec(
condition=td.VoltageBC(source=td.DCVoltageSource(voltage=[1])),
placement=td.StructureSimulationBoundary(structure="test_structure", surfaces=["z+", "z-"]),
)

bc2 = td.HeatChargeBoundarySpec(
condition=td.VoltageBC(source=td.DCVoltageSource(voltage=[0])),
placement=td.SimulationBoundary(surfaces=["z+", "x-"]),
)

sim = td.HeatChargeSimulation(
structures=[structure, structure2],
medium=td.Medium(heat_spec=td.FluidMedium()),
size=(2, 0, 2),
center=(0, 0, 0),
boundary_spec=[bc1, bc2],
grid_spec=td.UniformUnstructuredGrid(dl=0.1),
monitors=[
td.SteadyPotentialMonitor(
center=(0, 0, 0),
size=(td.inf, td.inf, td.inf),
name="potential_monitor",
unstructured=True,
),
td.SteadyFreeCarrierMonitor(
center=(0, 0, 0),
size=(1, 0, 1),
name="free_carrier_monitor",
unstructured=True,
),
],
analysis_spec=td.IsothermalSteadyChargeDCAnalysis(
temperature=300,
tolerance_settings=td.ChargeToleranceSpec(rel_tol=1e5, abs_tol=1e3, max_iters=400),
),
)

with pytest.raises(pd.ValidationError):
# Simulation must be at least 2D
_ = sim.updated_copy(size=(1, 0, 0))

changed_sim = sim._create_charge_copy_xy()

assert changed_sim.size == (2, 2, 0), "Size should be updated to (2, 2, 0)"

assert list(changed_sim.structures[0].medium.charge.N_d.coords["x"].data) == [0, 1, 2]
assert list(changed_sim.structures[0].medium.charge.N_d.coords["y"].data) == [0, 1, 2]
assert list(changed_sim.structures[0].medium.charge.N_d.coords["z"].data) == [0]

assert changed_sim.structures[0].medium.charge.N_a[0].source == "ymin"
assert changed_sim.structures[0].medium.charge.N_a[0].size == (1, 2, 1)
Copy link

Choose a reason for hiding this comment

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

logic: The assumption that the source should be 'ymin' is not explicitly tested. Add assertion for other source values ('ymax', 'xmin', 'xmax') to ensure complete rotation handling.


assert changed_sim.structures[0].geometry.size == (1, 3, 2)

assert changed_sim.boundary_spec[0].placement.surfaces == ("y+", "y-")

assert changed_sim.boundary_spec[1].placement.surfaces == ("y+", "x-")

assert changed_sim.monitors[0].size == (td.inf, td.inf, td.inf)
assert changed_sim.monitors[1].size == (1, 1, 0)
133 changes: 133 additions & 0 deletions tidy3d/components/tcad/simulation/heat_charge.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from __future__ import annotations

import copy
from enum import Enum
from typing import Optional, Union

Expand All @@ -23,6 +24,7 @@
StructureSimulationBoundary,
StructureStructureInterface,
)
from tidy3d.components.data.data_array import SpatialDataArray
from tidy3d.components.geometry.base import Box
from tidy3d.components.material.tcad.charge import (
ChargeConductorMedium,
Expand All @@ -42,6 +44,7 @@
HeatBoundarySpec,
HeatChargeBoundarySpec,
)
from tidy3d.components.tcad.doping import GaussianDoping
from tidy3d.components.tcad.grid import (
DistanceUnstructuredGrid,
UniformUnstructuredGrid,
Expand Down Expand Up @@ -583,6 +586,11 @@ def check_charge_simulation(cls, values):
"Currently, Charge simulations support only unstructured monitors. Please set "
f"monitor '{mnt.name}' to 'unstructured = True'."
)
zero_dims = mnt.zero_dims
if len(zero_dims) > 1:
raise SetupError(
f"Monitor '{mnt.name}' is a 1D monitor which are currently not supported in Charge."
)

return values

Expand Down Expand Up @@ -1778,3 +1786,128 @@ def _useHeatSourceFromConductionSim(self):
"""Returns True if 'HeatFromElectricSource' has been defined."""

return any(isinstance(source, HeatFromElectricSource) for source in self.sources)

def _create_charge_copy_xy(self) -> HeatChargeSimulation:
"""If a Charge simulation is not in the x-y plane we need to rotate it to that plane."""

sim_types = self._get_simulation_types()

if TCADAnalysisTypes.CHARGE not in sim_types:
return self

zero_dims = self.zero_dims

if len(zero_dims) > 1:
raise SetupError("Charge simulation can only be 2- or 3-D")

if len(zero_dims) == 0 or zero_dims[0] == 2:
return self

def _update_center_size(
zero_dims: list[int], center: list[float], size: list[float]
) -> tuple[list[float], list[float]]:
"""Update center and size based on zero dimensions."""
new_center = list(center)
new_size = list(size)

new_center[zero_dims[0]] = center[2]
new_center[2] = center[zero_dims[0]]

new_size[zero_dims[0]] = size[2]
new_size[2] = size[zero_dims[0]]

return new_center, new_size

# check doping boxes
# the following dictionary associates a structure with a modified medium
struct_medium = {}
for structure in self.structures:
if not isinstance(structure.medium.charge, SemiconductorMedium):
struct_medium[structure] = structure.medium
else:
sc_medium = copy.deepcopy(structure.medium.charge)

dopants = {"N_a": sc_medium.N_a, "N_d": sc_medium.N_d}
for key, dopant in dopants.items():
if isinstance(dopant, SpatialDataArray):
new_dopant = (
dopant.rename({"z": "z_tmp"})
.rename({["x", "y"][zero_dims[0]]: "z"})
.rename({"z_tmp": ["x", "y"][zero_dims[0]]})
)
sc_medium = sc_medium.updated_copy(**{key: new_dopant})
elif isinstance(dopant, tuple):
new_boxes = []
for doping_box in dopant:
if isinstance(doping_box, GaussianDoping):
new_center, new_size = _update_center_size(
zero_dims, doping_box.center, doping_box.size
)

source = doping_box.source
if "z" in source:
source = source.replace("z", ["x", "y"][zero_dims[0]])

new_boxes.append(
doping_box.updated_copy(
center=new_center, size=new_size, source=source
)
)
else:
new_boxes.append(doping_box)
sc_medium = sc_medium.updated_copy(**{key: new_boxes})
struct_medium[structure] = structure.medium.updated_copy(charge=sc_medium)

# change structures
new_structures = []
for structure in self.structures:
if not isinstance(structure.geometry, Box):
raise SetupError(
"2D Charge simulations defined in planes other than xy can only use Box geometries, "
)
else:
new_center, new_size = _update_center_size(
zero_dims, structure.geometry.center, structure.geometry.size
)

new_structures.append(
structure.updated_copy(
geometry=structure.geometry.updated_copy(center=new_center, size=new_size),
medium=struct_medium[structure],
)
)

# Boundary conditions
new_boundary_spec = []
for boundary in self.boundary_spec:
new_placement = boundary.placement
PlacementTypes = (SimulationBoundary, StructureSimulationBoundary)
if isinstance(boundary.placement, PlacementTypes):
surfaces = []
for s in boundary.placement.surfaces:
if "z" in s:
new_s = s.replace("z", ["x", "y"][zero_dims[0]])
surfaces.append(new_s)
else:
surfaces.append(s)
new_placement = new_placement.updated_copy(surfaces=surfaces)

new_boundary_spec.append(boundary.updated_copy(placement=new_placement))

# Monitors
new_monitors = []
for mnt in self.monitors:
new_center, new_size = _update_center_size(zero_dims, mnt.center, mnt.size)

new_monitors.append(mnt.updated_copy(center=new_center, size=new_size))

# simulation size
new_center, new_size = _update_center_size(zero_dims, self.center, self.size)

return self.updated_copy(
structures=new_structures,
boundary_spec=new_boundary_spec,
size=new_size,
center=new_center,
monitors=new_monitors,
)