From 5bcd47ed346a55fd90bb0cc52e1bf8a74202f04e Mon Sep 17 00:00:00 2001 From: Marc Bolinches Date: Fri, 27 Jun 2025 08:07:09 +0200 Subject: [PATCH 1/5] Adding a rotation function --- tests/test_components/test_heat_charge.py | 88 ++++++++++++++ .../components/tcad/simulation/heat_charge.py | 112 ++++++++++++++++++ 2 files changed, 200 insertions(+) diff --git a/tests/test_components/test_heat_charge.py b/tests/test_components/test_heat_charge.py index 985e6d5044..067360b96e 100644 --- a/tests/test_components/test_heat_charge.py +++ b/tests/test_components/test_heat_charge.py @@ -2039,3 +2039,91 @@ 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], + ) + + # 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", + ) + + # 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], + 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, + ) + ], + analysis_spec=td.IsothermalSteadyChargeDCAnalysis( + temperature=300, + tolerance_settings=td.ChargeToleranceSpec(rel_tol=1e5, abs_tol=1e3, max_iters=400), + ), + ) + + 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) + + 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-") diff --git a/tidy3d/components/tcad/simulation/heat_charge.py b/tidy3d/components/tcad/simulation/heat_charge.py index d5c90e31cc..eef407ea4b 100644 --- a/tidy3d/components/tcad/simulation/heat_charge.py +++ b/tidy3d/components/tcad/simulation/heat_charge.py @@ -3,6 +3,7 @@ from __future__ import annotations +import copy from enum import Enum from typing import Optional, Union @@ -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, @@ -42,6 +44,7 @@ HeatBoundarySpec, HeatChargeBoundarySpec, ) +from tidy3d.components.tcad.doping import GaussianDoping from tidy3d.components.tcad.grid import ( DistanceUnstructuredGrid, UniformUnstructuredGrid, @@ -1778,3 +1781,112 @@ 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 in sim_types: + zero_dims = self.zero_dims + if len(zero_dims) > 0: + if zero_dims[0] != 2: + # check doping boxes + # the following dictionary associates a structure with a modified medium + struct_medium = {} + for structure in self.structures: + if isinstance(structure.medium.charge, SemiconductorMedium): + 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 = list(doping_box.center) + new_center[zero_dims[0]] = doping_box.center[2] + new_center[2] = doping_box.center[zero_dims[0]] + + new_size = list(doping_box.size) + new_size[zero_dims[0]] = doping_box.size[2] + new_size[2] = doping_box.size[zero_dims[0]] + + 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 + ) + else: + struct_medium[structure] = structure.medium + + # change structures + new_structures = [] + for structure in self.structures: + new_center = list(structure.geometry.center) + new_center[zero_dims[0]] = structure.geometry.center[2] + new_center[2] = structure.geometry.center[zero_dims[0]] + + new_size = list(structure.geometry.size) + new_size[zero_dims[0]] = structure.geometry.size[2] + new_size[2] = structure.geometry.size[zero_dims[0]] + 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)) + + new_size = list(self.size) + new_size[zero_dims[0]] = self.size[2] + new_size[2] = self.size[zero_dims[0]] + + return self.updated_copy( + structures=new_structures, + boundary_spec=new_boundary_spec, + size=new_size, + ) + else: + return self + else: + return self + else: + return self From 2c6cb25b529c8e0b47cb523cbf8e70ba1c7a4ec0 Mon Sep 17 00:00:00 2001 From: Marc Bolinches Date: Mon, 7 Jul 2025 08:29:30 +0200 Subject: [PATCH 2/5] Add 2D test --- tests/test_components/test_heat_charge.py | 4 ++++ tidy3d/components/tcad/simulation/heat_charge.py | 4 +++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/test_components/test_heat_charge.py b/tests/test_components/test_heat_charge.py index 067360b96e..5722f3d390 100644 --- a/tests/test_components/test_heat_charge.py +++ b/tests/test_components/test_heat_charge.py @@ -2111,6 +2111,10 @@ def test_charge_copy_xy(): ), ) + 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)" diff --git a/tidy3d/components/tcad/simulation/heat_charge.py b/tidy3d/components/tcad/simulation/heat_charge.py index eef407ea4b..1bc84a8cb3 100644 --- a/tidy3d/components/tcad/simulation/heat_charge.py +++ b/tidy3d/components/tcad/simulation/heat_charge.py @@ -1789,7 +1789,7 @@ def _create_charge_copy_xy(self) -> HeatChargeSimulation: if TCADAnalysisTypes.CHARGE in sim_types: zero_dims = self.zero_dims - if len(zero_dims) > 0: + if len(zero_dims) == 1: if zero_dims[0] != 2: # check doping boxes # the following dictionary associates a structure with a modified medium @@ -1886,6 +1886,8 @@ def _create_charge_copy_xy(self) -> HeatChargeSimulation: ) else: return self + elif len(zero_dims) > 1: + raise SetupError("Charge simulation can only be 2- or 3-D") else: return self else: From 3a5a6de5a6ab19e7f5746145a761309fefcc0563 Mon Sep 17 00:00:00 2001 From: Marc Bolinches Date: Mon, 7 Jul 2025 19:21:22 +0200 Subject: [PATCH 3/5] Adding coordinate change for monitors and change to triangulardataset --- tests/test_components/test_heat_charge.py | 11 ++++++++++- tidy3d/components/data/unstructured/base.py | 15 ++++++++++----- tidy3d/components/data/unstructured/triangular.py | 7 ++++--- tidy3d/components/tcad/simulation/heat_charge.py | 15 +++++++++++++++ 4 files changed, 39 insertions(+), 9 deletions(-) diff --git a/tests/test_components/test_heat_charge.py b/tests/test_components/test_heat_charge.py index 5722f3d390..6012b6de38 100644 --- a/tests/test_components/test_heat_charge.py +++ b/tests/test_components/test_heat_charge.py @@ -2103,7 +2103,13 @@ def test_charge_copy_xy(): 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, @@ -2131,3 +2137,6 @@ def test_charge_copy_xy(): 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) diff --git a/tidy3d/components/data/unstructured/base.py b/tidy3d/components/data/unstructured/base.py index b5866fd2d6..9014623ba0 100644 --- a/tidy3d/components/data/unstructured/base.py +++ b/tidy3d/components/data/unstructured/base.py @@ -71,12 +71,17 @@ def _cell_num_vertices(cls) -> pd.PositiveInt: def points_right_dims(cls, val): """Check that point coordinates have the right dimensionality.""" # currently support only the standard axis ordering, that is 01(2) - axis_coords_expected = np.arange(cls._point_dims()) - axis_coords_given = val.axis.data - if np.any(axis_coords_given != axis_coords_expected): + # axis_coords_expected = np.arange(cls._point_dims()) + # axis_coords_given = val.axis.data + # if np.any(axis_coords_given != axis_coords_expected): + # raise ValidationError( + # f"Points array is expected to have {axis_coords_expected} coord values along 'axis'" + # f" (given: {axis_coords_given})." + # ) + if len(val.axis.data) != cls._point_dims(): raise ValidationError( - f"Points array is expected to have {axis_coords_expected} coord values along 'axis'" - f" (given: {axis_coords_given})." + f"Points array is expected to have {cls._point_dims()} coord values along 'axis'" + f" (given: {len(val.axis.data)})." ) return val diff --git a/tidy3d/components/data/unstructured/triangular.py b/tidy3d/components/data/unstructured/triangular.py index 6b8ccd098a..3e719d83bb 100644 --- a/tidy3d/components/data/unstructured/triangular.py +++ b/tidy3d/components/data/unstructured/triangular.py @@ -663,10 +663,11 @@ def plot( ) # set labels and titles + coords_indices = list(self.points.coords["axis"].data) ax_labels = ["x", "y", "z"] - normal_axis_name = ax_labels.pop(self.normal_axis) - ax.set_xlabel(ax_labels[0]) - ax.set_ylabel(ax_labels[1]) + normal_axis_name = ax_labels[self.normal_axis] + ax.set_xlabel(ax_labels[coords_indices[0]]) + ax.set_ylabel(ax_labels[coords_indices[1]]) ax.set_title(f"{normal_axis_name} = {self.normal_pos}") return ax diff --git a/tidy3d/components/tcad/simulation/heat_charge.py b/tidy3d/components/tcad/simulation/heat_charge.py index 1bc84a8cb3..0853a29606 100644 --- a/tidy3d/components/tcad/simulation/heat_charge.py +++ b/tidy3d/components/tcad/simulation/heat_charge.py @@ -1875,6 +1875,20 @@ def _create_charge_copy_xy(self) -> HeatChargeSimulation: new_boundary_spec.append(boundary.updated_copy(placement=new_placement)) + # Monitors + new_monitors = [] + for mnt in self.monitors: + new_center = list(mnt.center) + new_center[zero_dims[0]] = mnt.center[2] + new_center[2] = mnt.center[zero_dims[0]] + + new_size = list(mnt.size) + new_size[zero_dims[0]] = mnt.size[2] + new_size[2] = mnt.size[zero_dims[0]] + + new_monitors.append(mnt.updated_copy(center=new_center, size=new_size)) + + # simulation size new_size = list(self.size) new_size[zero_dims[0]] = self.size[2] new_size[2] = self.size[zero_dims[0]] @@ -1883,6 +1897,7 @@ def _create_charge_copy_xy(self) -> HeatChargeSimulation: structures=new_structures, boundary_spec=new_boundary_spec, size=new_size, + monitors=new_monitors, ) else: return self From c3d5226682c94b2f5c764a80b5538e92642bed05 Mon Sep 17 00:00:00 2001 From: Marc Bolinches Date: Tue, 8 Jul 2025 12:18:21 +0200 Subject: [PATCH 4/5] Revert TriangularDataset changes and adding validator for 1D monitors in backend --- tests/test_components/test_heat_charge.py | 8 ++++++++ tidy3d/components/data/unstructured/base.py | 15 +++++---------- tidy3d/components/data/unstructured/triangular.py | 7 +++---- tidy3d/components/tcad/simulation/heat_charge.py | 5 +++++ 4 files changed, 21 insertions(+), 14 deletions(-) diff --git a/tests/test_components/test_heat_charge.py b/tests/test_components/test_heat_charge.py index 6012b6de38..8f36836623 100644 --- a/tests/test_components/test_heat_charge.py +++ b/tests/test_components/test_heat_charge.py @@ -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 diff --git a/tidy3d/components/data/unstructured/base.py b/tidy3d/components/data/unstructured/base.py index 9014623ba0..b5866fd2d6 100644 --- a/tidy3d/components/data/unstructured/base.py +++ b/tidy3d/components/data/unstructured/base.py @@ -71,17 +71,12 @@ def _cell_num_vertices(cls) -> pd.PositiveInt: def points_right_dims(cls, val): """Check that point coordinates have the right dimensionality.""" # currently support only the standard axis ordering, that is 01(2) - # axis_coords_expected = np.arange(cls._point_dims()) - # axis_coords_given = val.axis.data - # if np.any(axis_coords_given != axis_coords_expected): - # raise ValidationError( - # f"Points array is expected to have {axis_coords_expected} coord values along 'axis'" - # f" (given: {axis_coords_given})." - # ) - if len(val.axis.data) != cls._point_dims(): + axis_coords_expected = np.arange(cls._point_dims()) + axis_coords_given = val.axis.data + if np.any(axis_coords_given != axis_coords_expected): raise ValidationError( - f"Points array is expected to have {cls._point_dims()} coord values along 'axis'" - f" (given: {len(val.axis.data)})." + f"Points array is expected to have {axis_coords_expected} coord values along 'axis'" + f" (given: {axis_coords_given})." ) return val diff --git a/tidy3d/components/data/unstructured/triangular.py b/tidy3d/components/data/unstructured/triangular.py index 3e719d83bb..6b8ccd098a 100644 --- a/tidy3d/components/data/unstructured/triangular.py +++ b/tidy3d/components/data/unstructured/triangular.py @@ -663,11 +663,10 @@ def plot( ) # set labels and titles - coords_indices = list(self.points.coords["axis"].data) ax_labels = ["x", "y", "z"] - normal_axis_name = ax_labels[self.normal_axis] - ax.set_xlabel(ax_labels[coords_indices[0]]) - ax.set_ylabel(ax_labels[coords_indices[1]]) + normal_axis_name = ax_labels.pop(self.normal_axis) + ax.set_xlabel(ax_labels[0]) + ax.set_ylabel(ax_labels[1]) ax.set_title(f"{normal_axis_name} = {self.normal_pos}") return ax diff --git a/tidy3d/components/tcad/simulation/heat_charge.py b/tidy3d/components/tcad/simulation/heat_charge.py index 0853a29606..7cc271f7fc 100644 --- a/tidy3d/components/tcad/simulation/heat_charge.py +++ b/tidy3d/components/tcad/simulation/heat_charge.py @@ -586,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 From 0d0298fdfaa6e0f0df0926104e5fab6648bbd92b Mon Sep 17 00:00:00 2001 From: Marc Bolinches Date: Wed, 9 Jul 2025 12:13:23 +0200 Subject: [PATCH 5/5] Addressing comments --- tests/test_components/test_heat_charge.py | 10 +- .../components/tcad/simulation/heat_charge.py | 233 +++++++++--------- 2 files changed, 124 insertions(+), 119 deletions(-) diff --git a/tests/test_components/test_heat_charge.py b/tests/test_components/test_heat_charge.py index 8f36836623..f48fc55d09 100644 --- a/tests/test_components/test_heat_charge.py +++ b/tests/test_components/test_heat_charge.py @@ -2077,7 +2077,7 @@ def test_charge_copy_xy(): mobility_n=td.ConstantMobilityModel(mu=1500), mobility_p=td.ConstantMobilityModel(mu=500), N_d=doping_da, - N_a=[d_box], + N_a=[d_box, d_box], ) # create a structure with the semiconductor medium @@ -2087,6 +2087,12 @@ def test_charge_copy_xy(): 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])), @@ -2099,7 +2105,7 @@ def test_charge_copy_xy(): ) sim = td.HeatChargeSimulation( - structures=[structure], + structures=[structure, structure2], medium=td.Medium(heat_spec=td.FluidMedium()), size=(2, 0, 2), center=(0, 0, 0), diff --git a/tidy3d/components/tcad/simulation/heat_charge.py b/tidy3d/components/tcad/simulation/heat_charge.py index 7cc271f7fc..f56ad98aff 100644 --- a/tidy3d/components/tcad/simulation/heat_charge.py +++ b/tidy3d/components/tcad/simulation/heat_charge.py @@ -1792,123 +1792,122 @@ def _create_charge_copy_xy(self) -> HeatChargeSimulation: sim_types = self._get_simulation_types() - if TCADAnalysisTypes.CHARGE in sim_types: - zero_dims = self.zero_dims - if len(zero_dims) == 1: - if zero_dims[0] != 2: - # check doping boxes - # the following dictionary associates a structure with a modified medium - struct_medium = {} - for structure in self.structures: - if isinstance(structure.medium.charge, SemiconductorMedium): - 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 = list(doping_box.center) - new_center[zero_dims[0]] = doping_box.center[2] - new_center[2] = doping_box.center[zero_dims[0]] - - new_size = list(doping_box.size) - new_size[zero_dims[0]] = doping_box.size[2] - new_size[2] = doping_box.size[zero_dims[0]] - - 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 - ) - else: - struct_medium[structure] = structure.medium - - # change structures - new_structures = [] - for structure in self.structures: - new_center = list(structure.geometry.center) - new_center[zero_dims[0]] = structure.geometry.center[2] - new_center[2] = structure.geometry.center[zero_dims[0]] - - new_size = list(structure.geometry.size) - new_size[zero_dims[0]] = structure.geometry.size[2] - new_size[2] = structure.geometry.size[zero_dims[0]] - new_structures.append( - structure.updated_copy( - geometry=structure.geometry.updated_copy( - center=new_center, size=new_size - ), - medium=struct_medium[structure], - ) + 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 + ) - # 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 = list(mnt.center) - new_center[zero_dims[0]] = mnt.center[2] - new_center[2] = mnt.center[zero_dims[0]] - - new_size = list(mnt.size) - new_size[zero_dims[0]] = mnt.size[2] - new_size[2] = mnt.size[zero_dims[0]] - - new_monitors.append(mnt.updated_copy(center=new_center, size=new_size)) - - # simulation size - new_size = list(self.size) - new_size[zero_dims[0]] = self.size[2] - new_size[2] = self.size[zero_dims[0]] - - return self.updated_copy( - structures=new_structures, - boundary_spec=new_boundary_spec, - size=new_size, - monitors=new_monitors, + new_structures.append( + structure.updated_copy( + geometry=structure.geometry.updated_copy(center=new_center, size=new_size), + medium=struct_medium[structure], ) - else: - return self - elif len(zero_dims) > 1: - raise SetupError("Charge simulation can only be 2- or 3-D") - else: - return self - else: - return self + ) + + # 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, + )