3D doping extrusion from a 2D carrier profile

Importing realistic doping profiles for charge transport and electro-optic simulations is typically a non-trivial task, as the doping geometries defined in physical layouts can be highly complex. In this notebook, we demonstrate how PhotonForge simplifies the process of loading and mapping 2D doping profiles directly onto these complex 3D layout geometries.

0ead997067a84c5ea1cdfb8e80feef50

This notebook demonstrates an end-to-end workflow to:

  • Define optical + electrical materials for Si / SiO2 / metal contacts

  • Extend a foundry technology with custom mask layers for doped regions and contacts

  • Build a simple waveguide component

  • Create a 2D carrier density profile

  • Extrude that 2D profile along a curved waveguide to form a full 3D donor/acceptor distribution

  • Attach the resulting 3D doping to Tidy3D structures for downstream charge and/or FDTD simulations

PhotonForge’s technology maps 2D layout masks → 3D extruded structures. We use that same mapping for geometry (where doped regions exist), and then attach physics data (spatially varying donor/acceptor densities) to the resulting semiconductor medium.

Data flow (what gets built):

  • Layout masks: waveguide + slab + doping masks + contact masks.

  • Extrusion specs: define which masks become which 3D materials (and their z-spans).

  • 2D carrier profile: arrays n_a(x,z) and n_d(x,z) (in cm⁻³).

  • 3D voxelization: build n_a(x,y,z) and n_d(x,y,z) on a global grid.

  • Medium update: wrap the 3D arrays in SpatialDataArray and inject them into a MultiPhysicsMedium.

  • Structure update: replace the placeholder “Doped Si” medium in the generated structure list.

Notes

  • The numeric values and profile functions are illustrative; replace them with your process data as needed.

  • This notebook focuses on how to wire the data into the technology + simulation objects.

Technology setup

We import PhotonForge, a reference technology, and Tidy3D. We also lower Tidy3D logging verbosity so the notebook output stays focused on the workflow.

[1]:
import matplotlib.pyplot as plt
import numpy as np
import photonforge as pf
import siepic_forge as siepic
import tidy3d as td
td.config.logging_level = "ERROR"

Define multiphysics materials (Si / SiO2 / contact)

We define:

  • silicon as a multiphysics semiconductor (optical permittivity + charge transport),

  • oxide as an insulator,

  • and a metal/contact medium for electrical terminals.

These material definitions will be referenced by the technology extrusion specs.

[2]:
charge_spec = td.SemiconductorMedium(
    permittivity=11.7,
    N_c=2.86e19,
    N_v=3.1e19,
    E_g=1.11,
    mobility_n=td.CaugheyThomasMobility(
        mu_min=52.2,
        mu=1471.0,
        ref_N=9.68e16,
        exp_N=0.68,
        exp_1=-0.57,
        exp_2=-2.33,
        exp_3=2.4,
        exp_4=-0.146,
    ),
    mobility_p=td.CaugheyThomasMobility(
        mu_min=44.9,
        mu=470.5,
        ref_N=2.23e17,
        exp_N=0.719,
        exp_1=-0.57,
        exp_2=-2.33,
        exp_3=2.4,
        exp_4=-0.146,
    ),
)

# Package optical and electrical definitions together for convenience.
si = {
    "optical": td.material_library["cSi"]["Li1993_293K"],
    "electrical": td.MultiPhysicsMedium(
        optical=td.Medium(permittivity=11.7),
        charge=charge_spec,
        name="Si",
    ),
}

# Metal contact: optical metal (Au) + an electrical conductor model.
contact_medium = {
    "optical": td.material_library["Au"]["JohnsonChristy1972"],
    "electrical": td.MultiPhysicsMedium(
        optical=td.PECMedium(
            viz_spec=td.VisualizationSpec(facecolor="#c2c4c3", alpha=1)
        ),
        charge=td.ChargeConductorMedium(conductivity=1),
        name="Contact",
    ),
}

# Oxide: electrical insulator + optical dielectric.
sio2 = {
    "optical": td.material_library["SiO2"]["Palik_Lossless"],
    "electrical": td.MultiPhysicsMedium(
        optical=td.Medium(permittivity=3.9),
        charge=td.ChargeInsulatorMedium(permittivity=3.9),
        name="SiO2",
    ),
}

Create a working technology instance

We start from a reference foundry technology (SiEPIC EBeam) and override its base media with the optical/electrical definitions from above. Then we set it as the default technology for subsequent PhotonForge components.

[3]:
tech = siepic.ebeam(si=si, sio2=sio2)

pf.config.default_technology = tech
pf.config.svg_labels = False

Then we add custom GDS mask layers for P / P++ and Contact. These layers are later used to decide where the 3D “Doped Si” and contact extrusions should exist.

[4]:
p_layer = pf.LayerSpec(
    layer=(21, 0), description="P Doping", color="#03fcd718", pattern="\\"
)

pp_layer = pf.LayerSpec(
    layer=(25, 0), description="P++ Doping", color="#0c7d5d18", pattern="\\"
)

contact_layer = pf.LayerSpec(
    layer=(14, 0), description="Metal_contact", color="#c2c4c318", pattern="solid"
)

tech.add_layer("Si P", p_layer)
tech.add_layer("Si P++", pp_layer)
tech.add_layer("Contact", contact_layer)
[4]:
Name: SiEPIC EBeam
Version: 0.4.32
Layers
NameLayerDescriptionColorPattern
Si(1, 0)Waveguides#ff80a818\\
PinRec(1, 10)SiEPIC#00408018/
PinRecM(1, 11)SiEPIC#00408018/
Si_Litho193nm(1, 69)Waveguides#cc80a818\
Waveguide(1, 99)Waveguides#ff80a818\
Si slab(2, 0)Waveguides#80a8ff18/
SiN(4, 0)Waveguides#a6cee318\\
Oxide open (to BOX)(6, 0)Waveguides#ffae0018\
Text(10, 0)#0000ff18\
M1_heater(11, 0)Metal#ebc63418xx
M2_router(12, 0)Metal#90857018xx
M_Open(13, 0)Metal#3471eb18xx
Contact(14, 0)Metal_contact#c2c4c318solid
Si N(20, 0)Doping#7000ff18\\
Si P(21, 0)P Doping#03fcd718\
Si N++(24, 0)Doping#0000ff18:
Si P++(25, 0)P++ Doping#0c7d5d18\
VC(40, 0)Metal#3a027f18xx
DevRec(68, 0)SiEPIC#00408018hollow
FbrTgt(81, 0)SiEPIC#00408018/
FloorPlan(99, 0)Misc#8000ff18hollow
SEM(200, 0)Misc#ff00ff18\
Deep Trench(201, 0)Misc#c0c0c018solid
Keep out(202, 0)Misc#a0a0c018//
Isolation Trench(203, 0)Misc#c0c0c018solid
Dicing(210, 0)Misc#a0a0c018solid
Chip design area(290, 0)Misc#80005718hollow
FDTD(733, 0)SiEPIC#80005718hollow
BlackBox(998, 0)SiEPIC#00408018solid
Errors(999, 0)SiEPIC#00008018/
Extrusion Specs
#MaskLimits (μm)Sidewal (°)Opt. MediumElec. Medium
0'Oxide open (to BOX)'0, inf0Medium(permittivity=1.0)Medium(permittivity=1.0)
1'Si'0, 0.220cSi_Li1993_293K
MultiPhysicsMedium(optical=……{'attrs': {}, 'name': None, 'frequency_range': None, 'allow_gain': False, 'nonlinear_spec': None, 'modulation_spec': None, 'viz_spec': None, 'heat_spec': None, 'type': 'Medium', 'permittivity': 11.7, 'conductivity': 0.0}, charge={'attrs': {}, 'name': None, 'frequency_range': None, 'allow_gain': False, 'nonlinear_spec': None, 'modulation_spec': None, 'viz_spec': None, 'heat_spec': None, 'type': 'SemiconductorMedium', 'permittivity': 11.7, 'N_c': {'attrs': {}, 'type': 'ConstantEffectiveDOS', 'N': 2.86e+19}, 'N_v': {'attrs': {}, 'type': 'ConstantEffectiveDOS', 'N': 3.1e+19}, 'E_g': {'attrs': {}, 'eg': 1.11, 'type': 'ConstantEnergyBandGap'}, 'mobility_n': {'attrs': {}, 'mu_min': 52.2, 'mu': 1471.0, 'exp_2': -2.33, 'exp_N': 0.68, 'ref_N': 9.68e+16, 'exp_1': -0.57, 'exp_3': 2.4, 'exp_4': -0.146, 'type': 'CaugheyThomasMobility'}, 'mobility_p': {'attrs': {}, 'mu_min': 44.9, 'mu': 470.5, 'exp_2': -2.33, 'exp_N': 0.719, 'ref_N': 2.23e+17, 'exp_1': -0.57, 'exp_3': 2.4, 'exp_4': -0.146, 'type': 'CaugheyThomasMobility'}, 'R': (), 'delta_E_g': None, 'N_a': (), 'N_d': ()}, name='Si')
2'Si slab'0, 0.090cSi_Li1993_293K
MultiPhysicsMedium(optical=……{'attrs': {}, 'name': None, 'frequency_range': None, 'allow_gain': False, 'nonlinear_spec': None, 'modulation_spec': None, 'viz_spec': None, 'heat_spec': None, 'type': 'Medium', 'permittivity': 11.7, 'conductivity': 0.0}, charge={'attrs': {}, 'name': None, 'frequency_range': None, 'allow_gain': False, 'nonlinear_spec': None, 'modulation_spec': None, 'viz_spec': None, 'heat_spec': None, 'type': 'SemiconductorMedium', 'permittivity': 11.7, 'N_c': {'attrs': {}, 'type': 'ConstantEffectiveDOS', 'N': 2.86e+19}, 'N_v': {'attrs': {}, 'type': 'ConstantEffectiveDOS', 'N': 3.1e+19}, 'E_g': {'attrs': {}, 'eg': 1.11, 'type': 'ConstantEnergyBandGap'}, 'mobility_n': {'attrs': {}, 'mu_min': 52.2, 'mu': 1471.0, 'exp_2': -2.33, 'exp_N': 0.68, 'ref_N': 9.68e+16, 'exp_1': -0.57, 'exp_3': 2.4, 'exp_4': -0.146, 'type': 'CaugheyThomasMobility'}, 'mobility_p': {'attrs': {}, 'mu_min': 44.9, 'mu': 470.5, 'exp_2': -2.33, 'exp_N': 0.719, 'ref_N': 2.23e+17, 'exp_1': -0.57, 'exp_3': 2.4, 'exp_4': -0.146, 'type': 'CaugheyThomasMobility'}, 'R': (), 'delta_E_g': None, 'N_a': (), 'N_d': ()}, name='Si')
3'SiN'0, 0.40Si3N4_Luke2015_PMLStableSi3N4
4
'M2_router' +…… 'M1_heater'
2.42, 2.620W_Werner2009
LossyMetalMedium……(frequency_range=(100000000.0, 200000000000.0), conductivity=1.6, fit_param={'attrs': {}, 'max_num_poles': 16, 'tolerance_rms': 0.001, 'frequency_sampling_points': 20, 'log_sampling': True, 'type': 'SurfaceImpedanceFitterParam'})
5'M2_router'2.62, 3.020Au_JohnsonChristy1972
LossyMetalMedium……(frequency_range=(100000000.0, 200000000000.0), conductivity=17.0, fit_param={'attrs': {}, 'max_num_poles': 16, 'tolerance_rms': 0.001, 'frequency_sampling_points': 20, 'log_sampling': True, 'type': 'SurfaceImpedanceFitterParam'})
6'M_Open'3.02, inf0Medium(permittivity=1.0)Medium(permittivity=1.0)
7
'Deep Trench' +…… 'Isolation Trench' + 'Dicing'
-inf, inf0Medium(permittivity=1.0)Medium(permittivity=1.0)
Ports
NameClassificationDescriptionWidth (μm)Limits (μm)Radius (μm)ModesTarget n_effPath profiles (μm)Voltage pathCurrent path
MM_SiN_TE_1550_3000optical
Multimode SiN Strip TE 1550 nm,…… w=3000 nm
8-2.5, 2.9072.1'SiN': 3
MM_TE_1550_2000optical
Multimode Strip TE 1550 nm,…… w=2000 nm
6-2, 2.220123.5'Si': 2
MM_TE_1550_3000optical
Multimode Strip TE 1550 nm,…… w=3000 nm
6-2, 2.220173.5'Si': 3
Rib_TE_1310_350optical
Rib (90 nm slab) TE 1310 nm,…… w=350 nm
2.35-1, 1.22013.5
'Si': 0.35, 'Si…… slab': 3
Rib_TE_1550_500optical
Rib (90 nm slab) TE 1550 nm,…… w=500 nm
2.5-1, 1.22013.5
'Si': 0.5, 'Si…… slab': 3
SiN_TE-TM_1550_1000opticalSiN Strip TM 1550 nm, w=1000 nm3-1.5, 1.9022.1'SiN': 1
SiN_TE_1310_750opticalSiN Strip TE 1310 nm, w=750 nm3-1, 1.4012.1'SiN': 0.75
SiN_TE_1310_800opticalSiN Strip TE 1310 nm, w=800 nm3-1, 1.4012.1'SiN': 0.8
SiN_TE_1550_1000opticalSiN Strip TE 1550 nm, w=1000 nm3-1, 1.4012.1'SiN': 1
SiN_TE_1550_750opticalSiN Strip TE 1550 nm, w=750 nm3-1, 1.4012.1'SiN': 0.75
SiN_TE_1550_800opticalSiN Strip TE 1550 nm, w=800 nm3-1, 1.4012.1'SiN': 0.8
SiN_TE_895_450opticalSiN Strip TE 895 nm, w=450 nm2-1, 1.4012.1'SiN': 0.45
SiN_TM_1310_750opticalSiN Strip TM 1310 nm, w=750 nm3-1.5, 1.901 + 1 (TM)2.1'SiN': 0.75
SiN_TM_1550_1000opticalSiN Strip TM 1550 nm, w=1000 nm3-1.5, 1.901 + 1 (TM)2.1'SiN': 1
Slot_TE_1550_500optical
Slot TE 1550 nm, w=500 nm,…… gap=100nm
2-1, 1.22013.5
'Si': 0.2 (-0.15),…… 'Si': 0.2 (+0.15)
TE-TM_1550_450opticalStrip TE-TM 1550, w=450 nm2-1, 1.22023.5'Si': 0.45
TE_1310_350opticalStrip TE 1310 nm, w=350 nm2-1, 1.22013.5'Si': 0.35
TE_1310_410opticalStrip TE 1310 nm, w=410 nm2-1, 1.22013.5'Si': 0.41
TE_1550_500opticalStrip TE 1550 nm, w=500 nm2-1, 1.22013.5'Si': 0.5
TM_1310_350opticalStrip TM 1310 nm, w=350 nm2-1, 1.2201 + 1 (TM)3.5'Si': 0.35
TM_1550_500opticalStrip TM 1550 nm, w=500 nm2.5-1, 1.2201 + 1 (TM)3.5'Si': 0.5
eskid_TE_1550opticaleskid TE 15503.31-1, 1.22013.5
'Si': 0.35,…… 'Si': 0.06 (+0.265), 'Si': 0.06 (-0.265), 'Si': 0.06 (+0.385), 'Si': 0.06 (-0.385), 'Si': 0.06 (+0.505), 'Si': 0.06 (-0.505), 'Si': 0.06 (+0.625), 'Si': 0.06 (-0.625)
Background medium
  • Optical: SiO2_Palik_Lossless
  • Electrical: MultiPhysicsMedium(optical={'attrs': {}, 'name': None, 'frequency_range': None, 'allow_gain': False, 'nonlinear_spec': None, 'modulation_spec': None, 'viz_spec': None, 'heat_spec': None, 'type': 'Medium', 'permittivity': 3.9, 'conductivity': 0.0}, charge={'attrs': {}, 'name': None, 'frequency_range': None, 'allow_gain': False, 'nonlinear_spec': None, 'modulation_spec': None, 'viz_spec': None, 'heat_spec': None, 'type': 'ChargeInsulatorMedium', 'permittivity': 3.9}, name='SiO2')
Connections: []

Next, we grab the silicon thickness, slab thickness, and metal thickness from the technology. These numbers define the z-extents used by the extrusion specs.

[5]:
si_thickness = tech.parametric_kwargs["si_thickness"]
slab_thickness = tech.parametric_kwargs["si_slab_thickness"]
metal_thickness = tech.parametric_kwargs["heater_thickness"]

Register doped-silicon + contact extrusions

Here we define a placeholder “Doped Si” medium (we’ll inject the spatial carrier densities later) and register extrusion specs so that:

  • doping mask layers map to “Doped Si” in the strip and slab regions (with appropriate z-spans),

  • the contact mask maps to a metal/contact medium above the slab.

[6]:
doped_si = {
    "optical": td.material_library["cSi"]["Li1993_293K"],
    "electrical": td.MultiPhysicsMedium(
        optical=td.Medium(permittivity=11.7),
        charge=charge_spec,
        name="Doped Si",
    ),
}

any_doping = pf.MaskSpec([(20, 0), (21, 0), (24, 0), (25, 0)])

doped_si_slab_mask = any_doping * (2, 0)
doped_si_strip_mask = any_doping * (1, 0)

contact_mask = pf.MaskSpec((14, 0))

for ee in [
    pf.ExtrusionSpec(doped_si_strip_mask, doped_si, (0, si_thickness)),
    pf.ExtrusionSpec(doped_si_slab_mask, doped_si, (0, slab_thickness)),
    pf.ExtrusionSpec(
        contact_mask,
        contact_medium,
        (
            slab_thickness,
            slab_thickness + metal_thickness,
        ),
    ),
]:
    tech.insert_extrusion_spec(len(tech.extrusion_specs), ee)

Inspect the technology object

Printing the tech object is a quick inspection step: you should see the new layers and extrusion specs appended to the base technology.

[7]:
tech
[7]:
Name: SiEPIC EBeam
Version: 0.4.32
Layers
NameLayerDescriptionColorPattern
Si(1, 0)Waveguides#ff80a818\\
PinRec(1, 10)SiEPIC#00408018/
PinRecM(1, 11)SiEPIC#00408018/
Si_Litho193nm(1, 69)Waveguides#cc80a818\
Waveguide(1, 99)Waveguides#ff80a818\
Si slab(2, 0)Waveguides#80a8ff18/
SiN(4, 0)Waveguides#a6cee318\\
Oxide open (to BOX)(6, 0)Waveguides#ffae0018\
Text(10, 0)#0000ff18\
M1_heater(11, 0)Metal#ebc63418xx
M2_router(12, 0)Metal#90857018xx
M_Open(13, 0)Metal#3471eb18xx
Contact(14, 0)Metal_contact#c2c4c318solid
Si N(20, 0)Doping#7000ff18\\
Si P(21, 0)P Doping#03fcd718\
Si N++(24, 0)Doping#0000ff18:
Si P++(25, 0)P++ Doping#0c7d5d18\
VC(40, 0)Metal#3a027f18xx
DevRec(68, 0)SiEPIC#00408018hollow
FbrTgt(81, 0)SiEPIC#00408018/
FloorPlan(99, 0)Misc#8000ff18hollow
SEM(200, 0)Misc#ff00ff18\
Deep Trench(201, 0)Misc#c0c0c018solid
Keep out(202, 0)Misc#a0a0c018//
Isolation Trench(203, 0)Misc#c0c0c018solid
Dicing(210, 0)Misc#a0a0c018solid
Chip design area(290, 0)Misc#80005718hollow
FDTD(733, 0)SiEPIC#80005718hollow
BlackBox(998, 0)SiEPIC#00408018solid
Errors(999, 0)SiEPIC#00008018/
Extrusion Specs
#MaskLimits (μm)Sidewal (°)Opt. MediumElec. Medium
0'Oxide open (to BOX)'0, inf0Medium(permittivity=1.0)Medium(permittivity=1.0)
1'Si'0, 0.220cSi_Li1993_293K
MultiPhysicsMedium(optical=……{'attrs': {}, 'name': None, 'frequency_range': None, 'allow_gain': False, 'nonlinear_spec': None, 'modulation_spec': None, 'viz_spec': None, 'heat_spec': None, 'type': 'Medium', 'permittivity': 11.7, 'conductivity': 0.0}, charge={'attrs': {}, 'name': None, 'frequency_range': None, 'allow_gain': False, 'nonlinear_spec': None, 'modulation_spec': None, 'viz_spec': None, 'heat_spec': None, 'type': 'SemiconductorMedium', 'permittivity': 11.7, 'N_c': {'attrs': {}, 'type': 'ConstantEffectiveDOS', 'N': 2.86e+19}, 'N_v': {'attrs': {}, 'type': 'ConstantEffectiveDOS', 'N': 3.1e+19}, 'E_g': {'attrs': {}, 'eg': 1.11, 'type': 'ConstantEnergyBandGap'}, 'mobility_n': {'attrs': {}, 'mu_min': 52.2, 'mu': 1471.0, 'exp_2': -2.33, 'exp_N': 0.68, 'ref_N': 9.68e+16, 'exp_1': -0.57, 'exp_3': 2.4, 'exp_4': -0.146, 'type': 'CaugheyThomasMobility'}, 'mobility_p': {'attrs': {}, 'mu_min': 44.9, 'mu': 470.5, 'exp_2': -2.33, 'exp_N': 0.719, 'ref_N': 2.23e+17, 'exp_1': -0.57, 'exp_3': 2.4, 'exp_4': -0.146, 'type': 'CaugheyThomasMobility'}, 'R': (), 'delta_E_g': None, 'N_a': (), 'N_d': ()}, name='Si')
2'Si slab'0, 0.090cSi_Li1993_293K
MultiPhysicsMedium(optical=……{'attrs': {}, 'name': None, 'frequency_range': None, 'allow_gain': False, 'nonlinear_spec': None, 'modulation_spec': None, 'viz_spec': None, 'heat_spec': None, 'type': 'Medium', 'permittivity': 11.7, 'conductivity': 0.0}, charge={'attrs': {}, 'name': None, 'frequency_range': None, 'allow_gain': False, 'nonlinear_spec': None, 'modulation_spec': None, 'viz_spec': None, 'heat_spec': None, 'type': 'SemiconductorMedium', 'permittivity': 11.7, 'N_c': {'attrs': {}, 'type': 'ConstantEffectiveDOS', 'N': 2.86e+19}, 'N_v': {'attrs': {}, 'type': 'ConstantEffectiveDOS', 'N': 3.1e+19}, 'E_g': {'attrs': {}, 'eg': 1.11, 'type': 'ConstantEnergyBandGap'}, 'mobility_n': {'attrs': {}, 'mu_min': 52.2, 'mu': 1471.0, 'exp_2': -2.33, 'exp_N': 0.68, 'ref_N': 9.68e+16, 'exp_1': -0.57, 'exp_3': 2.4, 'exp_4': -0.146, 'type': 'CaugheyThomasMobility'}, 'mobility_p': {'attrs': {}, 'mu_min': 44.9, 'mu': 470.5, 'exp_2': -2.33, 'exp_N': 0.719, 'ref_N': 2.23e+17, 'exp_1': -0.57, 'exp_3': 2.4, 'exp_4': -0.146, 'type': 'CaugheyThomasMobility'}, 'R': (), 'delta_E_g': None, 'N_a': (), 'N_d': ()}, name='Si')
3'SiN'0, 0.40Si3N4_Luke2015_PMLStableSi3N4
4
'M2_router' +…… 'M1_heater'
2.42, 2.620W_Werner2009
LossyMetalMedium……(frequency_range=(100000000.0, 200000000000.0), conductivity=1.6, fit_param={'attrs': {}, 'max_num_poles': 16, 'tolerance_rms': 0.001, 'frequency_sampling_points': 20, 'log_sampling': True, 'type': 'SurfaceImpedanceFitterParam'})
5'M2_router'2.62, 3.020Au_JohnsonChristy1972
LossyMetalMedium……(frequency_range=(100000000.0, 200000000000.0), conductivity=17.0, fit_param={'attrs': {}, 'max_num_poles': 16, 'tolerance_rms': 0.001, 'frequency_sampling_points': 20, 'log_sampling': True, 'type': 'SurfaceImpedanceFitterParam'})
6'M_Open'3.02, inf0Medium(permittivity=1.0)Medium(permittivity=1.0)
7
'Deep Trench' +…… 'Isolation Trench' + 'Dicing'
-inf, inf0Medium(permittivity=1.0)Medium(permittivity=1.0)
8
('Si N' + 'Si…… P' + 'Si N++' + 'Si P++') * 'Si'
0, 0.220cSi_Li1993_293K
MultiPhysicsMedium(optical=……{'attrs': {}, 'name': None, 'frequency_range': None, 'allow_gain': False, 'nonlinear_spec': None, 'modulation_spec': None, 'viz_spec': None, 'heat_spec': None, 'type': 'Medium', 'permittivity': 11.7, 'conductivity': 0.0}, charge={'attrs': {}, 'name': None, 'frequency_range': None, 'allow_gain': False, 'nonlinear_spec': None, 'modulation_spec': None, 'viz_spec': None, 'heat_spec': None, 'type': 'SemiconductorMedium', 'permittivity': 11.7, 'N_c': {'attrs': {}, 'type': 'ConstantEffectiveDOS', 'N': 2.86e+19}, 'N_v': {'attrs': {}, 'type': 'ConstantEffectiveDOS', 'N': 3.1e+19}, 'E_g': {'attrs': {}, 'eg': 1.11, 'type': 'ConstantEnergyBandGap'}, 'mobility_n': {'attrs': {}, 'mu_min': 52.2, 'mu': 1471.0, 'exp_2': -2.33, 'exp_N': 0.68, 'ref_N': 9.68e+16, 'exp_1': -0.57, 'exp_3': 2.4, 'exp_4': -0.146, 'type': 'CaugheyThomasMobility'}, 'mobility_p': {'attrs': {}, 'mu_min': 44.9, 'mu': 470.5, 'exp_2': -2.33, 'exp_N': 0.719, 'ref_N': 2.23e+17, 'exp_1': -0.57, 'exp_3': 2.4, 'exp_4': -0.146, 'type': 'CaugheyThomasMobility'}, 'R': (), 'delta_E_g': None, 'N_a': (), 'N_d': ()}, name='Doped Si')
9
('Si N' + 'Si…… P' + 'Si N++' + 'Si P++') * 'Si slab'
0, 0.090cSi_Li1993_293K
MultiPhysicsMedium(optical=……{'attrs': {}, 'name': None, 'frequency_range': None, 'allow_gain': False, 'nonlinear_spec': None, 'modulation_spec': None, 'viz_spec': None, 'heat_spec': None, 'type': 'Medium', 'permittivity': 11.7, 'conductivity': 0.0}, charge={'attrs': {}, 'name': None, 'frequency_range': None, 'allow_gain': False, 'nonlinear_spec': None, 'modulation_spec': None, 'viz_spec': None, 'heat_spec': None, 'type': 'SemiconductorMedium', 'permittivity': 11.7, 'N_c': {'attrs': {}, 'type': 'ConstantEffectiveDOS', 'N': 2.86e+19}, 'N_v': {'attrs': {}, 'type': 'ConstantEffectiveDOS', 'N': 3.1e+19}, 'E_g': {'attrs': {}, 'eg': 1.11, 'type': 'ConstantEnergyBandGap'}, 'mobility_n': {'attrs': {}, 'mu_min': 52.2, 'mu': 1471.0, 'exp_2': -2.33, 'exp_N': 0.68, 'ref_N': 9.68e+16, 'exp_1': -0.57, 'exp_3': 2.4, 'exp_4': -0.146, 'type': 'CaugheyThomasMobility'}, 'mobility_p': {'attrs': {}, 'mu_min': 44.9, 'mu': 470.5, 'exp_2': -2.33, 'exp_N': 0.719, 'ref_N': 2.23e+17, 'exp_1': -0.57, 'exp_3': 2.4, 'exp_4': -0.146, 'type': 'CaugheyThomasMobility'}, 'R': (), 'delta_E_g': None, 'N_a': (), 'N_d': ()}, name='Doped Si')
10'Contact'0.09, 0.290Au_JohnsonChristy1972
MultiPhysicsMedium(optical=……{'attrs': {}, 'name': None, 'frequency_range': None, 'allow_gain': False, 'nonlinear_spec': None, 'modulation_spec': None, 'viz_spec': {'attrs': {}, 'facecolor': '#c2c4c3', 'edgecolor': '#c2c4c3', 'alpha': 1.0, 'type': 'VisualizationSpec'}, 'heat_spec': None, 'type': 'PECMedium'}, charge={'attrs': {}, 'name': None, 'frequency_range': None, 'allow_gain': False, 'nonlinear_spec': None, 'modulation_spec': None, 'viz_spec': None, 'heat_spec': None, 'type': 'ChargeConductorMedium', 'permittivity': 1.0, 'conductivity': 1.0}, name='Contact')
Ports
NameClassificationDescriptionWidth (μm)Limits (μm)Radius (μm)ModesTarget n_effPath profiles (μm)Voltage pathCurrent path
MM_SiN_TE_1550_3000optical
Multimode SiN Strip TE 1550 nm,…… w=3000 nm
8-2.5, 2.9072.1'SiN': 3
MM_TE_1550_2000optical
Multimode Strip TE 1550 nm,…… w=2000 nm
6-2, 2.220123.5'Si': 2
MM_TE_1550_3000optical
Multimode Strip TE 1550 nm,…… w=3000 nm
6-2, 2.220173.5'Si': 3
Rib_TE_1310_350optical
Rib (90 nm slab) TE 1310 nm,…… w=350 nm
2.35-1, 1.22013.5
'Si': 0.35, 'Si…… slab': 3
Rib_TE_1550_500optical
Rib (90 nm slab) TE 1550 nm,…… w=500 nm
2.5-1, 1.22013.5
'Si': 0.5, 'Si…… slab': 3
SiN_TE-TM_1550_1000opticalSiN Strip TM 1550 nm, w=1000 nm3-1.5, 1.9022.1'SiN': 1
SiN_TE_1310_750opticalSiN Strip TE 1310 nm, w=750 nm3-1, 1.4012.1'SiN': 0.75
SiN_TE_1310_800opticalSiN Strip TE 1310 nm, w=800 nm3-1, 1.4012.1'SiN': 0.8
SiN_TE_1550_1000opticalSiN Strip TE 1550 nm, w=1000 nm3-1, 1.4012.1'SiN': 1
SiN_TE_1550_750opticalSiN Strip TE 1550 nm, w=750 nm3-1, 1.4012.1'SiN': 0.75
SiN_TE_1550_800opticalSiN Strip TE 1550 nm, w=800 nm3-1, 1.4012.1'SiN': 0.8
SiN_TE_895_450opticalSiN Strip TE 895 nm, w=450 nm2-1, 1.4012.1'SiN': 0.45
SiN_TM_1310_750opticalSiN Strip TM 1310 nm, w=750 nm3-1.5, 1.901 + 1 (TM)2.1'SiN': 0.75
SiN_TM_1550_1000opticalSiN Strip TM 1550 nm, w=1000 nm3-1.5, 1.901 + 1 (TM)2.1'SiN': 1
Slot_TE_1550_500optical
Slot TE 1550 nm, w=500 nm,…… gap=100nm
2-1, 1.22013.5
'Si': 0.2 (-0.15),…… 'Si': 0.2 (+0.15)
TE-TM_1550_450opticalStrip TE-TM 1550, w=450 nm2-1, 1.22023.5'Si': 0.45
TE_1310_350opticalStrip TE 1310 nm, w=350 nm2-1, 1.22013.5'Si': 0.35
TE_1310_410opticalStrip TE 1310 nm, w=410 nm2-1, 1.22013.5'Si': 0.41
TE_1550_500opticalStrip TE 1550 nm, w=500 nm2-1, 1.22013.5'Si': 0.5
TM_1310_350opticalStrip TM 1310 nm, w=350 nm2-1, 1.2201 + 1 (TM)3.5'Si': 0.35
TM_1550_500opticalStrip TM 1550 nm, w=500 nm2.5-1, 1.2201 + 1 (TM)3.5'Si': 0.5
eskid_TE_1550opticaleskid TE 15503.31-1, 1.22013.5
'Si': 0.35,…… 'Si': 0.06 (+0.265), 'Si': 0.06 (-0.265), 'Si': 0.06 (+0.385), 'Si': 0.06 (-0.385), 'Si': 0.06 (+0.505), 'Si': 0.06 (-0.505), 'Si': 0.06 (+0.625), 'Si': 0.06 (-0.625)
Background medium
  • Optical: SiO2_Palik_Lossless
  • Electrical: MultiPhysicsMedium(optical={'attrs': {}, 'name': None, 'frequency_range': None, 'allow_gain': False, 'nonlinear_spec': None, 'modulation_spec': None, 'viz_spec': None, 'heat_spec': None, 'type': 'Medium', 'permittivity': 3.9, 'conductivity': 0.0}, charge={'attrs': {}, 'name': None, 'frequency_range': None, 'allow_gain': False, 'nonlinear_spec': None, 'modulation_spec': None, 'viz_spec': None, 'heat_spec': None, 'type': 'ChargeInsulatorMedium', 'permittivity': 3.9}, name='SiO2')
Connections: []

Verify the technology-to-3D mapping

Before extruding any carrier profile, we build a small 2D test layout that includes Si, doped masks, and contacts. Plotting the resulting structures is a quick way to confirm that the mask layers and extrusion specs are wired correctly.

[8]:
c = pf.Component("Extrusion test", technology=tech)
c.add(
    "Si slab",
    pf.Rectangle((3, -1), (7.5, 1)),
    "Si",
    pf.Rectangle((5, -1), (5.5, 1)),
    "Si N",
    pf.Rectangle((4.2, -1), (5.25, 1)),
    "Si P",
    pf.Rectangle((5.25, -1), (6.3, 1)),
    "Si N++",
    pf.Rectangle((3, -1), (4.2, 1)),
    "Si P++",
    pf.Rectangle((6.3, -1), (7.5, 1)),
    "Contact",
    pf.Rectangle((3, -1), (4.2, 1)),
    pf.Rectangle((6.3, -1), (7.5, 1)),
)

c.add_model(pf.Tidy3DModel(medium=sio2["electrical"].optical))

# Visual sanity check: 2D cross-section plot of the generated 3D structures.
pf.tidy3d_plot(c, y=0, plot_type="structures", frequency=10e9)
plt.xlim(2.5, 8)
plt.ylim(-2, 2)
plt.show()
../_images/examples_Doping_Extrusion_16_0.png

Carrier density profile (2D)

In many workflows you already have a donor/acceptor distribution from:

  • TCAD / process simulation

  • Measured sheet resistance / implant models

  • A library of standard implant steps

For this example we generate a synthetic 2D profile so the rest of the pipeline is self-contained.

Conventions used below

  • x is the lateral coordinate across the waveguide.

  • z is the vertical coordinate (depth).

  • n_a(x, z) is the acceptor density (p-type).

  • n_d(x, z) is the donor density (n-type).

The profile is then extruded along a pf.Path in a later section.

[9]:
def f(x):
    """A smooth lateral profile used to separate p/n regions.

    This is a synthetic function for demonstration; replace with your imported data/model.
    """

    return 0.5 * (1 + 2 * np.arctan(x * 20) / np.pi) / (x**2 + 1)


def g(z):
    """A vertical (depth) profile envelope used for the implant distribution."""

    return np.exp(-(((z - 0.25) / 0.15) ** 4))


# Create a 2D grid for (x, z) and build donor/acceptor densities.
x, z = np.mgrid[-2:2:161j, 0:0.25:11j]

# Simple waveguide region mask (again: illustrative).
wg_mask = (abs(x) < 0.25) + (z < 0.09)

# Acceptors on +x, donors on -x (symmetry chosen for a junction-like profile).
n_a = 1e18 * f(x) * g(z) * wg_mask
n_d = 1e18 * f(-x) * g(z) * wg_mask


# Plot the acceptor profile on a log scale.
with np.errstate(divide="ignore"):
    plt.imshow(
        np.log10(n_a.T),
        origin="lower",
        extent=(x[0, 0], x[-1, -1], z[0, 0], z[-1, -1]),
    )
plt.colorbar()
plt.show()
../_images/examples_Doping_Extrusion_18_0.png

Simple component (a pf.Path waveguide)

To demonstrate extrusion along a curved structure, we construct a simple waveguide bend using Path class.

Why pf.Path?

  • It provides a continuous centerline parameterization.

  • It can be interpolated densely, giving the local tangent/normal direction needed to map the 2D profile onto global coordinates.

The same idea generalizes to other parameterized geometries (rings/disks, etc.) where you can write down the coordinate transform.

[10]:
component = pf.Component("Doped bend")

# Define the waveguide centerline as a path.
# (This could be any geometry expressible as a `pf.Path`.)
path = pf.Path((0, 0), 0.5)
path.segment((1, 0))
path.turn(angle=90, radius=12, euler_fraction=0.5)
path.segment((0, 1), relative=True)

# Add core + slab.
component.add("Si", path)
component.add("Si slab", path.updated_copy(width=5))

# Add doping mask layers as offset/width variants of the same path.
# These will be turned into 3D extrusions by the technology mapping configured earlier.
component.add("Si N", path.updated_copy(width=0.5, offset=0.25))
component.add("Si N++", path.updated_copy(width=1.5, offset=1.25))
component.add("Si P", path.updated_copy(width=0.5, offset=-0.25))
component.add("Si P++", path.updated_copy(width=1.5, offset=-1.25))

# Add an optical port so we can run a mode solve on the cross-section.
component.add_port(component.detect_ports(["Rib_TE_1550_500"]))

component
[10]:
../_images/examples_Doping_Extrusion_20_0.svg

Optional: optical mode sanity check

This step is not required for doping extrusion, but it’s a useful validation that the cross-section + port definition are consistent with the technology stack (you should see a reasonable guided mode).

[11]:
mode_solver = pf.port_modes(
    component.ports["P0"],
    frequencies=[pf.C_0 / 1.55],
    mesh_refinement=40,
    group_index=True,
)

mode_solver.plot_field("E", mode_index=0, f=pf.C_0 / 1.55)

mode_solver.data.to_dataframe()

Starting…
09:12:46 EST Loading simulation from local cache. View cached task using web UI
             at
             'https://tidy3d.simulation.cloud/workbench?taskId=mo-9eb0069a-7418-
             45b7-83e4-ead96cb660b6'.
Progress: 100%
[11]:
wavelength n eff k eff TE (Ey) fraction wg TE fraction wg TM fraction mode area group index dispersion (ps/(nm km))
f mode_index
1.934145e+14 0 1.55 2.564973 0.0 0.986683 0.868424 0.802951 0.203668 3.887829 -1550.035626
../_images/examples_Doping_Extrusion_22_4.png

Generate a reference Tidy3D structure list

PhotonForge can produce a Tidy3D simulation object from the component + technology. Here we generate a reference simulation mainly to access sim.structures, which we will later update to include spatially varying doping.

[12]:
sim = pf.Tidy3DModel(medium=sio2["electrical"].optical).get_simulations(
    component, frequencies=[10e9]
)

ax = sim.plot_structures(x=0.001)
ax.set_xlim(-3, 3)
ax.set_ylim(-0.1, 0.5)
plt.show()

../_images/examples_Doping_Extrusion_24_0.png

Generate the 3D doping distribution

Now we take the 2D cross-section profiles n_a(x, z) and n_d(x, z) and extrude them along the waveguide path.

Coordinate frames (what “extrusion” means here)

  • The 2D data lives in a local cross-section frame:

    • x: lateral coordinate across the waveguide (left/right)

    • z: vertical coordinate (depth)

  • The waveguide is described by a centerline pf.Path in the chip plane (global x,y).

At each point along the path we compute an in-plane moving frame:

  • tangent t = (t_x, t_y) (from the path gradient)

  • normal n = (-t_y, t_x) (perpendicular to the tangent)

Then a local point (x_local, z_local) maps to global coordinates as:

  • (x_global, y_global) = pos_xy + x_local * n

  • z_global = z_local

Voxelization strategy

Because the final data must be attached to a simulator as a 3D grid, we:

  • define a global voxel grid (xs, ys, zs) over the component bounds,

  • sample many pos_xy locations along the path,

  • “splat” the full n_a(x,z) / n_d(x,z) cross-section into the nearest voxel columns,

  • and average when multiple splats hit the same voxel.

This same strategy works for other spatial fields (temperature, stress, index perturbations) as long as you can define a local→global coordinate transform.

Voxelize the 2D cross-section into a 3D field along the path

This cell implements the extrusion algorithm: it samples points along the pf.Path, maps the local cross-section coordinate x into global (x,y) using the local normal direction, and accumulates n_a / n_d onto a global (xs,ys,zs) voxel grid.

[13]:
(xmin, ymin), (xmax, ymax) = component.bounds()

num_pts = 500
xs = np.linspace(xmin, xmax, num_pts)
ys = np.linspace(ymin, ymax, num_pts)

zs = z[0]

n_as = np.zeros((xs.size, ys.size, zs.size))
n_ds = np.zeros((xs.size, ys.size, zs.size))

counts = np.zeros((xs.size, ys.size, zs.size))

dx = xs[1] - xs[0]
dy = ys[1] - ys[0]
for pos, _, _, gradient in zip(
    *path.interpolate(np.linspace(0, path.length(), 10 * num_pts))
):
    # `gradient` is the local tangent direction t = (t_x, t_y).
    # The in-plane normal is n = (-t_y, t_x).
    #
    # Map each lateral cross-section coordinate x_local into global coordinates:
    # (x_global, y_global) = pos_xy + x_local * n
    x_local = x[:, 0]
    xt = pos[0] - gradient[1] * x_local - xmin
    yt = pos[1] + gradient[0] * x_local - ymin

    # Convert global coordinates into voxel indices.
    i = np.clip((xt / dx).round().astype(int), 0, xs.size - 1).tolist()
    j = np.clip((yt / dy).round().astype(int), 0, ys.size - 1).tolist()

    # Advanced indexing note:
    # `n_as[i, j, :]` has shape (len(x_local), len(zs)) and matches `n_a`'s (x,z) grid.
    n_as[i, j, :] += n_a
    n_ds[i, j, :] += n_d
    counts[i, j, :] += 1

# Avoid division by zero when averaging.
counts = np.clip(counts, 1, None)
n_as /= counts
n_ds /= counts

# Quick visualization: net doping sign on the z=0 slice (log-scaled donors minus acceptors).
plt.imshow(
    np.log10(n_ds[:, :, 0].T + 1e-10) - np.log10(n_as[:, :, 0].T + 1e-10),
    origin="lower",
    extent=(xmin, xmax, ymin, ymax),
    cmap="RdBu",
)
plt.colorbar()
plt.show()
../_images/examples_Doping_Extrusion_27_0.png

Generalizing the extrusion idea

Extruding a 2D field over a 3D structure is mainly about:

  • Defining the coordinate transform from a local cross-section frame to global coordinates

  • Sampling densely enough to avoid holes/aliasing in the final voxel grid

Any waveguide described as a pf.Path can use the algorithm above.

For rings/disks (or other analytical shapes), you can derive the transform directly from geometry and skip path interpolation entirely.

Attach the 3D doping to the semiconductor medium

We wrap the voxelized arrays n_d(x,y,z) and n_a(x,y,z) into td.SpatialDataArray objects and inject them into the semiconductor charge model. This produces a new MultiPhysicsMedium named “Doped Si” with spatially varying donor/acceptor densities.

[14]:
donors_array = td.SpatialDataArray(data=n_ds, coords={"x": xs, "y": ys, "z": zs})

acceptors_array = td.SpatialDataArray(data=n_as, coords={"x": xs, "y": ys, "z": zs})

doped_si = td.MultiPhysicsMedium(
    optical=td.Medium(permittivity=11.7),
    charge=charge_spec.updated_copy(N_d=donors_array, N_a=acceptors_array),
    name="Doped Si",
)

Here we swap the placeholder “Doped Si” medium inside the generated sim.structures with the updated one (containing spatially varying N_d(x,y,z) / N_a(x,y,z)), then plot a couple of cross-sections of the resulting doping field.

[15]:
structures = []
for s in sim.structures:
    if s.medium.name == "Doped Si":
        s = s.updated_copy(medium=doped_si)
    structures.append(s)

scene = td.Scene(structures=structures, medium=sio2["electrical"])

doping_range = [-1e17, 1e17]

scene.plot_structures_property(
    x=xs[1], property="doping", limits=doping_range, hlim=(-3, 3), vlim=(-0.1, 0.5)
)

scene.plot_structures_property(
    x=xs[350], property="doping", limits=doping_range, hlim=(0, 12), vlim=(-0.1, 0.5)
)

plt.show()

../_images/examples_Doping_Extrusion_32_0.png
../_images/examples_Doping_Extrusion_32_1.png

Next steps

At this point the 3D donor/acceptor distributions are embedded in the Tidy3D structures.

Typical follow-ups:

  • Run a charge simulation to compute carrier distributions under bias

  • Use the resulting perturbation in an FDTD simulation (e.g., resonance shift / phase shift)

  • Sweep process parameters (junction offset, peak concentration, anneal spread, etc.) by regenerating the 2D profile and repeating the extrusion

If you already have TCAD output, replace the synthetic n_a / n_d arrays with your imported data (making sure the coordinate convention matches) and keep the rest of the pipeline the same.