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 ​