Source code for tidy3d.components.tcad.doping

"""Semiconductor doping definitions."""

from __future__ import annotations

from typing import Union

import numpy as np
import pydantic.v1 as pd
import xarray as xr

from tidy3d.components.autograd import TracedSize
from tidy3d.components.base import cached_property
from tidy3d.components.data.data_array import SpatialDataArray
from tidy3d.components.geometry.base import Box
from tidy3d.constants import MICROMETER, PERCMCUBE, inf
from tidy3d.exceptions import SetupError


class AbstractDopingBox(Box):
    """Derived class from Box to deal with dopings"""

    # Override size so that we can set default values
    size: TracedSize = pd.Field(
        (inf, inf, inf),
        title="Size",
        description="Size in x, y, and z directions.",
        units=MICROMETER,
    )

    def _get_indices_in_box(self, coords: dict, meshgrid: bool = True):
        """Returns locations inside box"""

        # work out whether x,y, and z are present
        dim_missing = len(list(coords.keys())) < 3
        if dim_missing:
            for var_name in "xyz":
                if var_name not in coords:
                    coords[var_name] = [0]

        if meshgrid:
            X, Y, Z = np.meshgrid(coords["x"], coords["y"], coords["z"], indexing="ij")
        else:
            X = coords["x"]
            Y = coords["y"]
            Z = coords["z"]

        new_bounds = [list(self.bounds[0]), list(self.bounds[1])]
        for d in range(3):
            if new_bounds[0][d] == new_bounds[1][d]:
                new_bounds[0][d] = -np.inf
                new_bounds[1][d] = np.inf

        # let's assume some of these coordinates may lay outside the box
        indices_in_box = np.logical_and(new_bounds[0][0] <= X, new_bounds[1][0] >= X)
        indices_in_box = np.logical_and(indices_in_box, new_bounds[0][1] <= Y)
        indices_in_box = np.logical_and(indices_in_box, new_bounds[1][1] >= Y)
        indices_in_box = np.logical_and(indices_in_box, new_bounds[0][2] <= Z)
        indices_in_box = np.logical_and(indices_in_box, new_bounds[1][2] >= Z)

        return indices_in_box, X, Y, Z

    def _post_init_validators(self) -> None:
        # check the doping box is 3D
        if len(self.zero_dims) > 0:
            raise SetupError(
                "The doping box must be 3D. If you want a 2D doping box, please set one of the dimensions to a large or infinite size."
            )


[docs] class ConstantDoping(AbstractDopingBox): """ Sets constant doping :math:`N` in the specified box with a :py:attr:`~.Box.size` and :py:attr:`concentration`. For translationally invariant behavior in one dimension, the box must have infinite size in the homogenous (invariant) direction. Example ------- >>> import tidy3d as td >>> box_coords = [ ... [-1, -1, -1], ... [1, 1, 1] ... ] >>> constant_box1 = td.ConstantDoping(center=(0, 0, 0), size=(2, 2, 2), concentration=1e18) >>> constant_box2 = td.ConstantDoping.from_bounds(rmin=box_coords[0], rmax=box_coords[1], concentration=1e18) """ concentration: pd.NonNegativeFloat = pd.Field( default=0, title="Doping concentration density.", description="Doping concentration density.", units=PERCMCUBE, ) def _get_contrib(self, coords: dict, meshgrid: bool = True): """Returns the contribution to the doping a the locations specified in coords""" indices_in_box, X, _, _ = self._get_indices_in_box(coords=coords, meshgrid=meshgrid) contrib = np.zeros(X.shape) contrib[indices_in_box] = self.concentration return contrib.squeeze()
[docs] class GaussianDoping(AbstractDopingBox): """Sets a gaussian doping in the specified box. For translationally invariant behavior in one dimension, the box must have infinite size in the homogenous (invariant) direction. Notes ----- The Gaussian doping concentration :math:`N` is defined in the following manner: - :math:`N=N_{\\text{max}}` at locations more than :math:`\\text{width}` um away from the sides of the box. - :math:`N=N_{\\text{ref}}` at location on the box sides. - a Gaussian variation between :math:`N_{\\text{max}}` and :math:`N_{\\text{ref}}` at locations less than :math:`\\text{width}` um away from the sides. By definition, all sides of the box will have concentration :math:`N_{\\text{ref}}` (except the side specified as source) and the center of the box (:math:`\\text{width}` away from the box sides) will have a concentration :math:`N_{\\text{max}}`. .. math:: N = \\{N_{\\text{max}}\\} \\exp \\left[ - \\ln \\left( \\frac{\\{N_{\\text{max}}\\}}{\\{N_{\\text{ref}}\\}} \\right) \\left( \\frac{(x|y|z) - \\{(x|y|z)_{\\text{box}}\\}}{\\text{width}} \\right)^2 \\right] Example ------- >>> import tidy3d as td >>> box_coords = [ ... [-1, -1, -1], ... [1, 1, 1] ... ] >>> gaussian_box1 = td.GaussianDoping( ... center=(0, 0, 0), ... size=(2, 2, 2), ... ref_con=1e15, ... concentration=1e18, ... width=0.1, ... source="xmin" ... ) >>> gaussian_box2 = td.GaussianDoping.from_bounds( ... rmin=box_coords[0], ... rmax=box_coords[1], ... ref_con=1e15, ... concentration=1e18, ... width=0.1, ... source="xmin" ... ) """ ref_con: pd.PositiveFloat = pd.Field( title="Reference concentration.", description="Reference concentration. This is the minimum concentration in the box " "and it is attained at the edges/faces of the box.", units=PERCMCUBE, ) concentration: pd.PositiveFloat = pd.Field( title="Concentration", description="The concentration at the center of the box.", units=PERCMCUBE, ) width: pd.PositiveFloat = pd.Field( title="Width of the gaussian.", description="Width of the gaussian. The concentration will transition from " "``concentration`` at the center of the box to ``ref_con`` at the edge/face " "of the box in a distance equal to ``width``. ", units=MICROMETER, ) source: str = pd.Field( "xmin", title="Source face", description="Specifies the side of the box acting as the source, i.e., " "the face specified does not have a gaussian evolution normal to it, instead " "the concentration is constant from this face. Accepted values for ``source`` " "are [``xmin``, ``xmax``, ``ymin``, ``ymax``, ``zmin``, ``zmax``]", ) @cached_property def sigma(self): """The sigma parameter of the pseudo-gaussian""" return np.sqrt(-self.width * self.width / 2 / np.log(self.ref_con / self.concentration)) def _get_contrib(self, coords: dict, meshgrid: bool = True): """Returns the contribution to the doping a the locations specified in coords""" indices_in_box, X, Y, Z = self._get_indices_in_box(coords=coords, meshgrid=meshgrid) x_contrib = np.zeros(X.shape) x_contrib[indices_in_box] = 1.0 if self.source != "xmin": x0 = self.bounds[0][0] indices = np.logical_and(x0 <= X, x0 + self.width >= X) indices = np.logical_and(indices, indices_in_box) x_contrib[indices] = np.exp( -(X[indices] - x0 - self.width) * (X[indices] - x0 - self.width) / 2 / self.sigma / self.sigma ) # higher x face if self.source != "xmax": x1 = self.bounds[1][0] indices = np.logical_and(x1 - self.width <= X, x1 >= X) indices = np.logical_and(indices, indices_in_box) x_contrib[indices] = np.exp( -(X[indices] - x1 + self.width) * (X[indices] - x1 + self.width) / 2 / self.sigma / self.sigma ) y_contrib = np.zeros(X.shape) y_contrib[indices_in_box] = 1.0 if self.source != "ymin": y0 = self.bounds[0][1] indices = np.logical_and(y0 <= Y, y0 + self.width >= Y) indices = np.logical_and(indices, indices_in_box) y_contrib[indices] = np.exp( -(Y[indices] - y0 - self.width) * (Y[indices] - y0 - self.width) / 2 / self.sigma / self.sigma ) # higher y face if self.source != "ymax": y1 = self.bounds[1][1] indices = np.logical_and(y1 - self.width <= Y, y1 >= Y) indices = np.logical_and(indices, indices_in_box) y_contrib[indices] = np.exp( -(Y[indices] - y1 + self.width) * (Y[indices] - y1 + self.width) / 2 / self.sigma / self.sigma ) z_contrib = np.zeros(X.shape) z_contrib[indices_in_box] = 1.0 if self.source != "zmin": z0 = self.bounds[0][2] indices = np.logical_and(z0 <= Z, z0 + self.width >= Z) indices = np.logical_and(indices, indices_in_box) z_contrib[indices] = np.exp( -(Z[indices] - z0 - self.width) * (Z[indices] - z0 - self.width) / 2 / self.sigma / self.sigma ) # higher z face if self.source != "zmax": z1 = self.bounds[1][2] indices = np.logical_and(z1 - self.width <= Z, z1 >= Z) indices = np.logical_and(indices, indices_in_box) z_contrib[indices] = np.exp( -(Z[indices] - z1 + self.width) * (Z[indices] - z1 + self.width) / 2 / self.sigma / self.sigma ) total_contrib = x_contrib * y_contrib * z_contrib * self.concentration return total_contrib.squeeze()
class CustomDoping(AbstractDopingBox): """Sets a custom doping in the specified box. Example ------- >>> import tidy3d as td >>> import numpy as np >>> box_coords = [ ... [-1, -1, -1], ... [1, 1, 1] ... ] >>> x = np.linspace(-1, 1, 5) >>> y = np.linspace(-1, 1, 5) >>> z = np.linspace(-1, 1, 5) >>> data = np.random.rand(5, 5, 5)*1e18 >>> concentration = td.SpatialDataArray( ... data=data, ... coords={'x': x, 'y': y, 'z': z}, ... ) >>> custom_box1 = td.CustomDoping( ... center=(0, 0, 0), ... size=(2, 2, 2), ... concentration=concentration ... ) >>> custom_box2 = td.CustomDoping.from_bounds( ... rmin=box_coords[0], ... rmax=box_coords[1], ... concentration=concentration ... ) """ concentration: SpatialDataArray = pd.Field( ..., title="Doping concentration data array.", description="Doping concentration data array.", units=PERCMCUBE, ) def _get_contrib(self, coords: dict, meshgrid: bool = True): """Returns the contribution to the doping a the locations specified in coords""" indices_in_box, X, Y, Z = self._get_indices_in_box(coords=coords, meshgrid=meshgrid) contrib = np.zeros(X.shape) # interpolate if meshgrid: interp_result = self.concentration.interp(coords) contrib[indices_in_box] = interp_result.values[indices_in_box] else: # X, Y, Z are 1D arrays of coordinates # interp_result = self.concentration.interp(coords) # contrib = np.zeros(X.shape) # contrib[indices_in_box] = interp_result.values[indices_in_box] interp_coords = coords interp_da = { name: xr.DataArray(data, dims="new_dim") for name, data in interp_coords.items() } interp_res = self.concentration.interp( **interp_da, kwargs={"fill_value": 0, "bounds_error": False} ) contrib[indices_in_box] = interp_res.values[indices_in_box] return contrib.squeeze() DopingBoxType = Union[ConstantDoping, GaussianDoping, CustomDoping]