Thermal

Thermal#

You can use our FEM femwell plugin for thermal simulations. You can simulate directly the component layout and include important effects such as metal dummy fill.

import gdsfactory as gf
import meshio
from gdsfactory.generic_tech import get_generic_pdk
from gdsfactory.technology import LayerStack
from skfem.io import from_meshio

from gplugins.gmsh import create_physical_mesh, get_mesh

gf.config.rich_output()
PDK = get_generic_pdk()
PDK.activate()
gf.CONF.display_type = "klayout"

LAYER_STACK = PDK.layer_stack

LAYER_STACK.layers["heater"].thickness = 0.13
LAYER_STACK.layers["heater"].zmin = 2.2

heater = gf.components.straight_heater_metal(length=50)
heater.plot()

../_images/566582eaf9b8244f4d5dad2654e6e96a4419cb552b0c13991228c68f36918dd3.png
print(LAYER_STACK.layers.keys())
dict_keys(['substrate', 'box', 'core', 'shallow_etch', 'deep_etch', 'clad', 'slab150', 'slab90', 'nitride', 'ge', 'undercut', 'via_contact', 'metal1', 'heater', 'via1', 'metal2', 'via2', 'metal3'])
filtered_layer_stack = LayerStack(
    layers={
        k: gf.pdk.get_layer_stack().layers[k]
        for k in ("slab90", "core", "via_contact", "heater")
    }
)
filename = "mesh"


def mesh_with_physicals(mesh, filename):
    mesh_from_file = meshio.read(f"{filename}.msh")
    return create_physical_mesh(mesh_from_file, "triangle")
mesh = get_mesh(
    component=heater,
    type="uz",
    xsection_bounds=[(4, -4), (4, 4)],
    layer_stack=filtered_layer_stack,
    filename=f"{filename}.msh",
)
mesh = mesh_with_physicals(mesh, filename)
mesh = from_meshio(mesh)
mesh.draw().plot()
Info    : [  0%] Difference                                                                                  
Info    : [ 10%] Difference - Performing intersection of shapes                                                                                
Info    : [ 80%] Difference - Building splits of containers                                                                                
Info    : [  0%] Fragments                                                                                  
Info    : [ 10%] Fragments - Performing intersection of shapes                                                                                
Info    : [ 80%] Fragments - Building splits of containers                                                                                
Info    : Meshing 1D...
Info    : [  0%] Meshing curve 3 (Line)
Info    : [  0%] Meshing curve 1 (Line)
Info    : [  0%] Meshing curve 2 (Line)
Info    : [  0%] Meshing curve 4 (Line)
Info    : [ 20%] Meshing curve 5 (Line)
Info    : [ 20%] Meshing curve 6 (Line)
Info    : [ 40%] Meshing curve 7 (Line)
Info    : [ 40%] Meshing curve 8 (Line)
Info    : [ 60%] Meshing curve 9 (Line)
Info    : [ 60%] Meshing curve 10 (Line)
Info    : Done meshing 1D (Wall 0.00275126s, CPU 0.006052s)
Info    : Meshing 2D...
Info    : [  0%] Meshing surface 2 (Plane, Frontal-Delaunay)
Info    : [  0%] Meshing surface 1 (Plane, Frontal-Delaunay)
Info    : Done meshing 2D (Wall 0.00251243s, CPU 0.00873s)
Info    : 36 nodes 78 elements
Info    : Writing 'mesh.msh'...
Info    : Done writing 'mesh.msh'
Info    : Writing '/tmp/tmphrl3mpyi/mesh.msh'...
Info    : Done writing '/tmp/tmphrl3mpyi/mesh.msh'

[]

../_images/1e616b63b3a5db800f299fe16e280466ecb28a305fb5494654f3ff61e41caa1c.png

Example based on femwell

from collections import OrderedDict

import matplotlib.pyplot as plt
import numpy as np
from femwell.maxwell.waveguide import compute_modes
from femwell.mesh import mesh_from_OrderedDict
from femwell.thermal import solve_thermal
from shapely.geometry import LineString, Polygon
from skfem import Basis, ElementTriP0
from skfem.io import from_meshio
from tqdm import tqdm
w_sim = 8 * 2
h_clad = 2.8
h_box = 2
w_core = 0.5
h_core = 0.22
h_heater = 0.14
w_heater = 2
offset_heater = 2 + (h_core + h_heater) / 2
h_silicon = 0.5

polygons = OrderedDict(
    bottom=LineString(
        [
            (-w_sim / 2, -h_core / 2 - h_box - h_silicon),
            (w_sim / 2, -h_core / 2 - h_box - h_silicon),
        ]
    ),
    core=Polygon(
        [
            (-w_core / 2, -h_core / 2),
            (-w_core / 2, h_core / 2),
            (w_core / 2, h_core / 2),
            (w_core / 2, -h_core / 2),
        ]
    ),
    heater=Polygon(
        [
            (-w_heater / 2, -h_heater / 2 + offset_heater),
            (-w_heater / 2, h_heater / 2 + offset_heater),
            (w_heater / 2, h_heater / 2 + offset_heater),
            (w_heater / 2, -h_heater / 2 + offset_heater),
        ]
    ),
    clad=Polygon(
        [
            (-w_sim / 2, -h_core / 2),
            (-w_sim / 2, -h_core / 2 + h_clad),
            (w_sim / 2, -h_core / 2 + h_clad),
            (w_sim / 2, -h_core / 2),
        ]
    ),
    box=Polygon(
        [
            (-w_sim / 2, -h_core / 2),
            (-w_sim / 2, -h_core / 2 - h_box),
            (w_sim / 2, -h_core / 2 - h_box),
            (w_sim / 2, -h_core / 2),
        ]
    ),
    wafer=Polygon(
        [
            (-w_sim / 2, -h_core / 2 - h_box - h_silicon),
            (-w_sim / 2, -h_core / 2 - h_box),
            (w_sim / 2, -h_core / 2 - h_box),
            (w_sim / 2, -h_core / 2 - h_box - h_silicon),
        ]
    ),
)

resolutions = dict(
    core={"resolution": 0.04, "distance": 1},
    clad={"resolution": 0.6, "distance": 1},
    box={"resolution": 0.6, "distance": 1},
    heater={"resolution": 0.1, "distance": 1},
)

mesh = from_meshio(
    mesh_from_OrderedDict(polygons, resolutions, default_resolution_max=0.6)
)
mesh.draw().show()

../_images/803160d72789f3ec98661b045ac8a520a703eb8067e7e2a50b9398154cb4ee43.png

And then we solve it!

currents = np.linspace(0.0, 7.4e-3, 10)
current_densities = currents / polygons["heater"].area
neffs = []

for current_density in tqdm(current_densities):
    basis0 = Basis(mesh, ElementTriP0(), intorder=4)
    thermal_conductivity_p0 = basis0.zeros()
    for domain, value in {
        "core": 90,
        "box": 1.38,
        "clad": 1.38,
        "heater": 28,
        "wafer": 148,
    }.items():
        thermal_conductivity_p0[basis0.get_dofs(elements=domain)] = value
    thermal_conductivity_p0 *= 1e-12  # 1e-12 -> conversion from 1/m^2 -> 1/um^2

    basis, temperature = solve_thermal(
        basis0,
        thermal_conductivity_p0,
        specific_conductivity={"heater": 2.3e6},
        current_densities={"heater": current_density},
        fixed_boundaries={"bottom": 0},
    )

    if current_density == current_densities[-1]:
        basis.plot(temperature, shading="gouraud", colorbar=True)
        plt.show()

    temperature0 = basis0.project(basis.interpolate(temperature))
    epsilon = basis0.zeros() + (1.444 + 1.00e-5 * temperature0) ** 2
    epsilon[basis0.get_dofs(elements="core")] = (
        3.4777 + 1.86e-4 * temperature0[basis0.get_dofs(elements="core")]
    ) ** 2
    # basis0.plot(epsilon, colorbar=True).show()

    modes = compute_modes(basis0, epsilon, wavelength=1.55, num_modes=1, solver="scipy")

    if current_density == current_densities[-1]:
        modes[0].show(modes[0].E.real)

    neffs.append(np.real(modes[0].n_eff))

print(f"Phase shift: {2 * np.pi / 1.55 * (neffs[-1] - neffs[0]) * 320}")
plt.xlabel("Current / mA")
plt.ylabel("Effective refractive index $n_{eff}$")
plt.plot(currents * 1e3, neffs)
plt.show()
  0%|          | 0/10 [00:00<?, ?it/s]
 10%|█         | 1/10 [00:00<00:01,  4.53it/s]
 20%|██        | 2/10 [00:00<00:01,  4.55it/s]
 30%|███       | 3/10 [00:00<00:01,  4.56it/s]
 40%|████      | 4/10 [00:00<00:01,  4.50it/s]
 50%|█████     | 5/10 [00:01<00:01,  4.53it/s]
 60%|██████    | 6/10 [00:01<00:00,  4.56it/s]
 70%|███████   | 7/10 [00:01<00:00,  4.56it/s]
 80%|████████  | 8/10 [00:01<00:00,  4.58it/s]
 90%|█████████ | 9/10 [00:01<00:00,  4.57it/s]

../_images/453d2f683b391fd4074cdf98390b7a0a9e972a28fbd844fc4420d298736991a8.png
2024-05-10 10:17:33.353 | WARNING  | __main__:<module>:40 - DeprecationWarning: The behavior of passing an array directly to `show` is deprecated and will be removed in the future. Use `plot` instead.

../_images/91b37f731c607f5069f6a41090a78e738cbd69f6f941ea7f047b99a9d6570007.png
100%|██████████| 10/10 [00:02<00:00,  2.45it/s]
100%|██████████| 10/10 [00:02<00:00,  3.56it/s]
Phase shift: 3.786321638790277


../_images/0701f8aabe9deb81f27fa5cd77b35e6540b2db9bb6bb077eca613787c50ce61a.png