Skip to content

BSI 2x2 Basic

A complete recipe for simulating a standard 2x2 Bayer BSI pixel unit cell and computing QE spectra.

Interactive BSI Pixel Stack Cross-Section

Click on any layer to view its material properties and role in the pixel stack.

Incident lightAir(semi-infinite)Microlens0.4 - 0.8 umPlanarization0.3 - 1.0 umColor Filter0.4 - 0.8 umBARL0.05 - 0.12 umDTIDTISilicon2.0 - 4.0 umSubstrate / Metal(semi-infinite)
Click a layer in the stack diagram to see its properties.

Interactive Bayer Pattern Viewer

Explore different color filter array (CFA) patterns. Click a pixel to see its details.

R(0,0)G(0,1)G(1,0)B(1,1)R(0,2)G(0,3)G(1,2)B(1,3)R(2,0)G(2,1)G(3,0)B(3,1)R(2,2)G(2,3)G(3,2)B(3,3)Unit cell
Pattern
RGGB
Unit Cell Size
2x2
Green Ratio
50%
Description
Most common Bayer pattern. Two green pixels per unit cell provide higher luminance resolution, mimicking human vision sensitivity.

What you will learn

  • Setting up a full BSI pixel configuration from scratch
  • Running a wavelength sweep with RCWA
  • Plotting per-channel QE spectra
  • Validating energy balance

Prerequisites

bash
pip install -e ".[rcwa]"

Interactive Pixel Stack Builder

Adjust the thickness of each layer in a BSI pixel cross-section. The visualization shows the vertical stack with refractive indices and a scale bar.

Total stack height:4.58 um
0.01.02.03.04.0Microlens0.60 umn=1.56Planarization0.30 umn=1.46Color Filter0.60 umn=1.55n=1.8Silicon3.00 umn=3.5DTI

Configuration

Create a config dictionary (or load from YAML):

python
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": {
                "thickness": 0.6,
                "pattern": "bayer_rggb",
                "materials": {"R": "cf_red", "G": "cf_green", "B": "cf_blue"},
                "grid": {"enabled": True, "width": 0.05, "material": "tungsten"},
            },
            "barl": {
                "layers": [
                    {"thickness": 0.010, "material": "sio2"},
                    {"thickness": 0.025, "material": "hfo2"},
                    {"thickness": 0.015, "material": "sio2"},
                    {"thickness": 0.030, "material": "si3n4"},
                ]
            },
            "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": [9, 9], "dtype": "complex64"},
        "stability": {
            "precision_strategy": "mixed",
            "allow_tf32": False,
            "fourier_factorization": "li_inverse",
        },
    },
    "source": {
        "type": "planewave",
        "wavelength": {
            "mode": "sweep",
            "sweep": {"start": 0.40, "stop": 0.70, "step": 0.01},
        },
        "angle": {"theta_deg": 0.0, "phi_deg": 0.0},
        "polarization": "unpolarized",
    },
    "compute": {"backend": "auto"},
}

Run the simulation

python
from compass.runners.single_run import SingleRunner

result = SingleRunner.run(config)

print(f"Wavelengths: {result.wavelengths.shape[0]} points "
      f"({result.wavelengths[0]*1000:.0f}-{result.wavelengths[-1]*1000:.0f} nm)")
print(f"Pixels: {list(result.qe_per_pixel.keys())}")

Plot QE spectrum

python
from compass.visualization.qe_plot import plot_qe_spectrum
import matplotlib.pyplot as plt

fig, ax = plt.subplots(figsize=(8, 5))
plot_qe_spectrum(result, ax=ax)
ax.set_title("BSI 2x2 Bayer (1 um pitch) - QE Spectrum")
plt.tight_layout()
plt.savefig("bsi_2x2_qe.png", dpi=150)
plt.show()

Check energy balance

python
from compass.analysis.energy_balance import EnergyBalance

check = EnergyBalance.check(result, tolerance=0.02)
print(f"Valid: {check['valid']}, max error: {check['max_error']:.4f}")

Energy Balance: R + T + A = 1

Adjust silicon thickness and BARL quality to see how reflection, transmission, and absorption partition the incident light across the visible spectrum.

Max error:0.00e+0
Mean error:0.00e+0
Validation:✓ Pass
Peak absorption:380 nm
0.00.20.40.60.81.0400450500550600650700750Wavelength (nm)FractionReflection (R)Transmission (T)Absorption (A)

Extract per-channel QE

python
from compass.analysis.qe_calculator import QECalculator

channel_qe = QECalculator.spectral_response(result.qe_per_pixel, result.wavelengths)

for color, (wl, qe) in channel_qe.items():
    peak_idx = qe.argmax()
    print(f"{color}: peak QE = {qe[peak_idx]:.1%} at {wl[peak_idx]*1000:.0f} nm")

Expected output (approximate):

B: peak QE = 62.3% at 460 nm
G: peak QE = 71.5% at 540 nm
R: peak QE = 59.8% at 620 nm

Interactive QE Spectrum Chart

Explore how silicon thickness, BARL quality, and metal grid width affect the quantum efficiency spectrum of Red, Green, and Blue channels.

Blue peak QE:46.7%
Green peak QE:54.5%
Red peak QE:42.8%
Average QE:48.0%
0%20%40%60%80%400450500550600650700750Wavelength (nm)QE (%)BlueGreenRed

Variations to try

Smaller pitch (0.8 um)

python
config["pixel"]["pitch"] = 0.8
config["pixel"]["layers"]["microlens"]["radius_x"] = 0.38
config["pixel"]["layers"]["microlens"]["radius_y"] = 0.38
config["pixel"]["layers"]["silicon"]["photodiode"]["size"] = [0.55, 0.55, 1.6]

Thicker silicon (4 um)

python
config["pixel"]["layers"]["silicon"]["thickness"] = 4.0
config["pixel"]["layers"]["silicon"]["dti"]["depth"] = 4.0
config["pixel"]["layers"]["silicon"]["photodiode"]["size"] = [0.7, 0.7, 3.0]

No microlens

python
config["pixel"]["layers"]["microlens"]["enabled"] = False

Compare QE with and without microlens to quantify its contribution.