Skip to content

Metal Grid Effect

This recipe demonstrates how the tungsten metal grid between color filter sub-pixels affects QE and optical crosstalk.

Background

The metal grid sits between adjacent color filter elements. It provides optical isolation by blocking light from entering neighboring pixels through the color filter layer. However, it also:

  • Reduces the effective aperture (less light enters each pixel)
  • Can cause diffraction effects at the grid edges
  • Absorbs some light (tungsten is lossy)

This recipe runs two simulations -- with and without the metal grid -- and compares the results.

BSI Pixel Cross-Section Anatomy

Select any layer to highlight it and view a detailed description below. An animated light ray traces the optical path.

Incident light (-z)AirMicrolensPlanarizationGRColor FilterMetal GridBARLSiliconPDPDPhotodiodeDTIz
Select a layer in the diagram to learn about its role in the pixel stack.

Setup

python
import copy
from compass.runners.single_run import SingleRunner
from compass.analysis.solver_comparison import SolverComparison
from compass.visualization.qe_plot import plot_qe_comparison, plot_crosstalk_heatmap
import matplotlib.pyplot as plt

base_config = {
    "pixel": {
        "pitch": 1.0,
        "unit_cell": [2, 2],
        "bayer_map": [["R", "G"], ["G", "B"]],
        "layers": {
            "air": {"thickness": 1.0, "material": "air"},
            "microlens": {
                "enabled": True, "height": 0.6,
                "radius_x": 0.48, "radius_y": 0.48,
                "material": "polymer_n1p56",
                "profile": {"type": "superellipse", "n": 2.5, "alpha": 1.0},
                "shift": {"mode": "none"},
            },
            "planarization": {"thickness": 0.3, "material": "sio2"},
            "color_filter": {
                "pattern": "bayer_rggb",
                "red": {"material": "cf_red", "thickness": 0.62, "contact_angle": 66.0},
                "green": {"material": "cf_green", "thickness": 0.60, "contact_angle": 72.0},
                "blue": {"material": "cf_blue", "thickness": 0.65, "contact_angle": 62.0},
                "grid": {"enabled": True, "width": 0.05, "thickness": 0.47, "material": "tungsten"},
            },
            "barl": {
                "layers": [
                    {"thickness": 0.010, "material": "sio2"},
                    {"thickness": 0.025, "material": "hfo2"},
                ]
            },
            "silicon": {
                "thickness": 3.0, "material": "silicon",
                "photodiode": {"position": [0, 0, 0.5], "size": [0.7, 0.7, 2.0]},
                "dti": {"enabled": True, "width": 0.1, "material": "sio2"},
            },
        },
    },
    "solver": {
        "name": "torcwa", "type": "rcwa",
        "params": {"fourier_order": [11, 11]},
        "stability": {"precision_strategy": "mixed", "fourier_factorization": "li_inverse"},
    },
    "source": {
        "wavelength": {"mode": "sweep", "sweep": {"start": 0.40, "stop": 0.70, "step": 0.01}},
        "polarization": "unpolarized",
    },
    "compute": {"backend": "auto"},
}

Run: with metal grid

python
config_with_grid = copy.deepcopy(base_config)
config_with_grid["pixel"]["layers"]["color_filter"]["grid"]["enabled"] = True

result_with = SingleRunner.run(config_with_grid)
print("With grid: done")

Run: without metal grid

python
config_no_grid = copy.deepcopy(base_config)
config_no_grid["pixel"]["layers"]["color_filter"]["grid"]["enabled"] = False

result_without = SingleRunner.run(config_no_grid)
print("Without grid: done")

Compare QE spectra

python
fig, ax = plt.subplots(figsize=(10, 6))
plot_qe_comparison(
    results=[result_with, result_without],
    labels=["With grid", "No grid"],
    ax=ax,
)
ax.set_title("Metal Grid Effect on QE")
plt.tight_layout()
plt.savefig("metal_grid_qe_comparison.png", dpi=150)
plt.show()

Quantify the difference

python
comparison = SolverComparison(
    results=[result_with, result_without],
    labels=["with_grid", "no_grid"],
    reference_idx=0,
)
summary = comparison.summary()

for key, val in summary["max_qe_diff"].items():
    print(f"{key}: max |dQE| = {val:.4f}")

Compare crosstalk

python
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 6))

plot_crosstalk_heatmap(result_with, ax=ax1)
ax1.set_title("Crosstalk: With Grid")

plot_crosstalk_heatmap(result_without, ax=ax2)
ax2.set_title("Crosstalk: No Grid")

plt.tight_layout()
plt.savefig("metal_grid_crosstalk.png", dpi=150)
plt.show()

Interactive Pixel Crosstalk Heatmap

Explore how wavelength and pixel pitch affect optical crosstalk between neighboring pixels. The center pixel is illuminated; surrounding pixels show crosstalk intensity.

Absorption Depth (Si):1.56 um
Nearest-neighbor Crosstalk:3.8%
Total Crosstalk:55.2%
1.3%(0,0)R1.8%(0,1)G2.1%(0,2)R1.8%(0,3)G1.3%(0,4)R1.8%(1,0)G3.0%(1,1)B3.8%(1,2)G3.0%(1,3)B1.8%(1,4)G2.1%(2,0)R3.8%(2,1)GSource(2,2)R3.8%(2,3)G2.1%(2,4)R1.8%(3,0)G3.0%(3,1)B3.8%(3,2)G3.0%(3,3)B1.8%(3,4)G1.3%(4,0)R1.8%(4,1)G2.1%(4,2)R1.8%(4,3)G1.3%(4,4)R0%4%Crosstalk

Expected observations

  1. QE reduction with grid: The metal grid slightly reduces peak QE (typically 2-5%) because it blocks some light and absorbs energy.
  2. Crosstalk improvement: The grid significantly reduces optical crosstalk between adjacent pixels, especially for off-axis illumination.
  3. Wavelength dependence: The grid effect is stronger at shorter wavelengths where diffraction effects are more pronounced relative to the grid width.

Grid width sweep

Study how grid width affects the QE/crosstalk trade-off:

python
import numpy as np

grid_widths = [0.0, 0.03, 0.05, 0.08, 0.10]
results_vs_width = []

for width in grid_widths:
    cfg = copy.deepcopy(base_config)
    cfg["pixel"]["layers"]["color_filter"]["grid"]["enabled"] = width > 0
    cfg["pixel"]["layers"]["color_filter"]["grid"]["width"] = width
    r = SingleRunner.run(cfg)
    results_vs_width.append(r)
    print(f"Grid width {width*1000:.0f} nm: done")

# Plot
fig, ax = plt.subplots(figsize=(10, 6))
plot_qe_comparison(
    results=results_vs_width,
    labels=[f"w={w*1000:.0f}nm" for w in grid_widths],
    ax=ax,
)
ax.set_title("QE vs Metal Grid Width")
plt.tight_layout()
plt.savefig("grid_width_sweep.png", dpi=150)

Reproducibility

Numbers shown above are illustrative outputs from one specific run. They depend on solver version, materials, hardware (GPU vs CPU, fp32 vs fp64), and config. Always re-run the recipe in your environment to validate before drawing conclusions.

Rounded-rectangle CF corners

Real CF photolithography rounds the four corners of each color filter cell. Set grid.corner_radius to model each CF as a rounded rectangle (same r at all four corners); the metal grid is then the complement within the unit cell. corner_radius = 0 keeps the original sharp-cornered grid.

python
corner_radii = [0.0, 0.05, 0.10, 0.15]
results_vs_radius = []

for r in corner_radii:
    cfg = copy.deepcopy(base_config)
    cfg["pixel"]["layers"]["color_filter"]["grid"]["enabled"] = True
    cfg["pixel"]["layers"]["color_filter"]["grid"]["width"] = 0.05
    cfg["pixel"]["layers"]["color_filter"]["grid"]["corner_radius"] = r
    results_vs_radius.append(SingleRunner.run(cfg))
    print(f"corner_radius {r*1000:.0f} nm: done")

fig, ax = plt.subplots(figsize=(10, 6))
plot_qe_comparison(
    results=results_vs_radius,
    labels=[f"r={r*1000:.0f}nm" for r in corner_radii],
    ax=ax,
)
ax.set_title("QE vs CF corner radius (sharp -> rounded)")
plt.tight_layout()
plt.savefig("grid_corner_radius_sweep.png", dpi=150)

Increasing r enlarges the metal area near the four-pixel intersections, so the trend usually mirrors a small effective grid-width increase: peak QE drops slightly while diagonal-neighbor crosstalk drops more than edge-neighbor crosstalk. r is auto-clamped to (pitch - grid.width) / 2; at that limit the CF degenerates into an inscribed circle.

See also