At the probe station you capture one IV curve per device, one device per die, one die per wafer. The natural place to store the width and length is in the filename, but the moment a second operator runs the same experiment the abbreviations diverge (R_500_10.csv vs res_w500nm_l10um.csv), and collecting all the width variants for a single die later requires either grep gymnastics or a separate mapping spreadsheet. Tagging each file at upload time solves this: the physical dimensions travel with the data, not in its name.

In the previous notebook we generated a GDS layout with three resistance test structures of different widths and uploaded it to DataLab.

In a real experiment you would now probe each structure on a wafer using a four-probe measurement to get IV curves, then extract resistance from the slope. Here we simulate that process: for each combination of wafer, die, and device, we compute a synthetic IV curve using Ohm's law, add realistic noise and defects, and upload the result with provenance tags.

The tags on each file are what make the data queryable later. Each file carries: the project name, your username, the wafer ID, the die coordinates, the cell name (which encodes the width), the device instance name, the physical length, and the physical width. The analysis notebooks will use these tags to group measurements and extract sheet resistance.

Setup

import getpass
from contextlib import suppress
from functools import cache
from itertools import product

import gdsfactory as gf
import gfhub
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from gdsfactory.gpdk import PDK
from tqdm.notebook import tqdm

PDK.activate()
np.random.seed(42)
client = gfhub.Client()
user = getpass.getuser()
print(f"Running as user: {user}")
Running as user: runner

The measurement model

Each IV measurement sweeps current from 0 to 10 mA and records voltage. The resistance of each structure follows R = Rs × L / W, where Rs is the sheet resistance (100 Ω/sq for this simulation), L is the structure length, and W is the width. Voltage is then just V = R × I (Ohm's law).

To make the data realistic, we add three layers of variation:

  • Wafer-to-wafer variation: up to ±20% multiplicative shift, constant across all dies on one wafer. This mimics run-to-run process variation.
  • Device-to-device variation: up to ±5% additional random factor per device, representing local within-die non-uniformity.
  • Measurement noise: 5% random multiplicative noise on each voltage sample.
  • Defects: a small fraction of devices are simulated as open circuits (R = 1 GΩ) or short circuits (R = 0), which the analysis pipeline will need to handle.
def iv_resistance(resistance_ohm: float, current_mA: np.ndarray) -> np.ndarray:
    """Return voltage in mV for a given resistance and current sweep (Ohm's law)."""
    return resistance_ohm * current_mA  # mV = Ω × mA


# Show a sample IV curve for a 30 Ω resistor
df_demo = pd.DataFrame(
    {
        "current_mA": (i := np.linspace(0, 10, 21)),
        "voltage_mV": iv_resistance(resistance_ohm=30, current_mA=i),
    }
)

plt.plot(df_demo.current_mA, df_demo.voltage_mV)
plt.title("IV Curve (30 Ω resistor)")
plt.xlabel("Current (mA)")
plt.ylabel("Voltage (mV)")
plt.grid(True)
plt.show()

png

Wafer and die definitions

We simulate three wafers and a 5×5 grid of dies with the four corner dies removed. The die coordinates range from (-2,-2) to (2,2).

wafer_ids = ["wafer1", "wafer2"]

dies = [
    {"x": x, "y": y}
    for y in range(-1, 1)
    for x in range(-1, 1)
]

Load the layout

We import the GDS generated in notebook 1 to read the device geometry. Each instance in the top-level cell has cell.info.width and cell.info.length attributes set by resistance_sheet, so we can read the physical dimensions directly from the layout rather than hardcoding them.

@cache
def load_gds():
    return gf.import_gds("resistance.gds", skip_new_cells=True)


gds = load_gds()
gds

png

Clean up existing files (optional)

Delete any files from a previous run of this notebook so you start fresh. The GDS uploaded in notebook 1 is preserved.

existing_files = client.query_files(tags=["project:tutorial_resistance", user])
to_delete = [f for f in existing_files if f["original_name"] != "resistance.gds"]
for file in tqdm(to_delete):
    with suppress(RuntimeError):
        client.delete_file(file["id"])
        print(f"Deleted file {file["original_name"]}")
  0%|          | 0/138 [00:00<?, ?it/s]


Deleted file wafer_sheet_resistance.png
Deleted file wafer_sheet_resistance.png
Deleted file die_sheet_resistance.png


Deleted file die_sheet_resistance.png
Deleted file die_sheet_resistance.json
Deleted file die_sheet_resistance.json
Deleted file die_sheet_resistance.png
Deleted file die_sheet_resistance.json


Deleted file die_sheet_resistance.png
Deleted file die_sheet_resistance.json
Deleted file die_sheet_resistance.png
Deleted file die_sheet_resistance.png


Deleted file die_sheet_resistance.json
Deleted file die_sheet_resistance.json
Deleted file die_sheet_resistance.png
Deleted file die_sheet_resistance.json
Deleted file die_sheet_resistance.png
Deleted file die_sheet_resistance.json


Deleted file iv_measurement_linear_fit.png
Deleted file iv_measurement_linear_fit.json
Deleted file iv_measurement_linear_fit.png
Deleted file iv_measurement_linear_fit.json
Deleted file iv_measurement_linear_fit.png
Deleted file iv_measurement_linear_fit.json


Deleted file iv_measurement_linear_fit.png
Deleted file iv_measurement_linear_fit.png
Deleted file iv_measurement_linear_fit.json


Deleted file iv_measurement_linear_fit.png
Deleted file iv_measurement_linear_fit.json
Deleted file iv_measurement_linear_fit.json
Deleted file iv_measurement_linear_fit.png
Deleted file iv_measurement_linear_fit.json
Deleted file iv_measurement_linear_fit.png
Deleted file iv_measurement_linear_fit.json
Deleted file iv_measurement_linear_fit.png
Deleted file iv_measurement_linear_fit.json
Deleted file iv_measurement_linear_fit.png
Deleted file iv_measurement_linear_fit.json
Deleted file iv_measurement_linear_fit.png
Deleted file iv_measurement_linear_fit.json
Deleted file iv_measurement_linear_fit.png
Deleted file iv_measurement_linear_fit.png
Deleted file iv_measurement_linear_fit.json
Deleted file iv_measurement_linear_fit.json
Deleted file iv_measurement_linear_fit.png
Deleted file iv_measurement_linear_fit.png
Deleted file iv_measurement_linear_fit.json
Deleted file iv_measurement_linear_fit.json
Deleted file iv_measurement_linear_fit.png
Deleted file iv_measurement_linear_fit.png
Deleted file iv_measurement_linear_fit.json
Deleted file iv_measurement_linear_fit.json
Deleted file iv_measurement_linear_fit.png
Deleted file iv_measurement_linear_fit.png
Deleted file iv_measurement_linear_fit.json


Deleted file iv_measurement_linear_fit.json
Deleted file iv_measurement_linear_fit.png
Deleted file iv_measurement_linear_fit.png
Deleted file iv_measurement_linear_fit.json
Deleted file iv_measurement_linear_fit.json
Deleted file iv_measurement_linear_fit.png
Deleted file iv_measurement_linear_fit.json
Deleted file iv_measurement_linear_fit.png
Deleted file iv_measurement_linear_fit.json
Deleted file iv_measurement_linear_fit.png
Deleted file iv_measurement_linear_fit.png
Deleted file iv_measurement_linear_fit.json
Deleted file iv_measurement_linear_fit.json
Deleted file iv_measurement_linear_fit.png
Deleted file iv_measurement_linear_fit.json
Deleted file iv_measurement_linear_fit.png
Deleted file iv_measurement_linear_fit.json
Deleted file iv_measurement_linear_fit.png
Deleted file iv_measurement_linear_fit.json
Deleted file iv_measurement_linear_fit.png
Deleted file iv_measurement_linear_fit.json
Deleted file iv_measurement_linear_fit.png
Deleted file iv_measurement_linear_fit.json
Deleted file iv_measurement_linear_fit.png
Deleted file iv_measurement_linear_fit.json
Deleted file iv_measurement_linear_fit.png
Deleted file iv_measurement_linear_fit.png
Deleted file iv_measurement_linear_fit.json
Deleted file iv_measurement_linear_fit.json
Deleted file iv_measurement_linear_fit.png
Deleted file iv_measurement_linear_fit.png
Deleted file iv_measurement_linear_fit.json
Deleted file iv_measurement_linear_fit.png
Deleted file iv_measurement_linear_fit.json
Deleted file iv_measurement_linear_fit.json
Deleted file iv_measurement_linear_fit.png
Deleted file iv_measurement_linear_fit.json
Deleted file iv_measurement_linear_fit.png
Deleted file iv_measurement_linear_fit.json
Deleted file iv_measurement_linear_fit.png
Deleted file iv_measurement_linear_fit.json
Deleted file iv_measurement_linear_fit.png
Deleted file iv_measurement_linear_fit.json
Deleted file iv_measurement_linear_fit.png
Deleted file iv_measurement_linear_fit.json
Deleted file iv_measurement_linear_fit.png
Deleted file iv_measurement_linear_fit.json
Deleted file iv_measurement_linear_fit.png
Deleted file iv_measurement_linear_fit.json
Deleted file iv_measurement_linear_fit.png
Deleted file iv_measurement_linear_fit.json
Deleted file iv_measurement_linear_fit.png
Deleted file iv_measurement_linear_fit.json
Deleted file iv_measurement_linear_fit.png
Deleted file iv_measurement_linear_fit.json
Deleted file iv_measurement_linear_fit.png
Deleted file iv_measurement_linear_fit.json
Deleted file iv_measurement_linear_fit.png
Deleted file iv_measurement_linear_fit.json
Deleted file iv_measurement.parquet
Deleted file iv_measurement.parquet
Deleted file iv_measurement.parquet
Deleted file iv_measurement.parquet
Deleted file iv_measurement.parquet
Deleted file iv_measurement.parquet
Deleted file iv_measurement.parquet
Deleted file iv_measurement.parquet
Deleted file iv_measurement.parquet
Deleted file iv_measurement.parquet
Deleted file iv_measurement.parquet
Deleted file iv_measurement.parquet
Deleted file iv_measurement.parquet
Deleted file iv_measurement.parquet
Deleted file iv_measurement.parquet
Deleted file iv_measurement.parquet
Deleted file iv_measurement.parquet
Deleted file iv_measurement.parquet
Deleted file iv_measurement.parquet
Deleted file iv_measurement.parquet
Deleted file iv_measurement.parquet
Deleted file iv_measurement.parquet
Deleted file iv_measurement.parquet
Deleted file iv_measurement.parquet

Generate and upload measurements

For each combination of wafer, die, and device instance we generate a synthetic IV curve and upload it. The cell tag uses the cell name from the GDS (which encodes the width), and width and length tags carry the physical dimensions in µm so downstream analysis can recover the sheet resistance formula without needing to re-read the layout. Because the dimensions are tags rather than parts of a filename, any downstream query can use query_files(tags=["width:10", "die:0,0"]) without parsing filenames or maintaining a lookup table.

sheet_resistance = 100  # Ω/sq
current_sweep = np.linspace(0, 10, 21)  # mA

for wafer, die in tqdm(list(product(wafer_ids, dies))):
    # Per-wafer multiplicative shift (±20%)
    wafer_factor = 1 + 0.20 * (2 * np.random.rand() - 1)
    die_id = f"{die['x']},{die['y']}"

    for inst in gds.insts:
        width_um = inst.cell.info.width
        length_um = inst.cell.info.length

        # Nominal resistance from sheet resistance formula: R = Rs × L / W
        nominal_R = sheet_resistance * length_um / width_um

        # Apply device-to-device variation (±5%)
        device_factor = 1 + 0.05 * (2 * np.random.rand() - 1)
        resistance = nominal_R * wafer_factor * device_factor

        # Simulate defects: 5% open circuits, 3% short circuits
        defect_roll = np.random.rand()
        if defect_roll < 0.05:
            resistance = 1e9  # open circuit
        elif defect_roll < 0.08:
            resistance = 0    # short circuit

        voltage_mV = iv_resistance(resistance_ohm=resistance, current_mA=current_sweep)

        # Add 5% multiplicative noise per sample
        noise = 0.05 * (2 * np.random.rand(len(voltage_mV)) - 1) * voltage_mV
        voltage_mV = voltage_mV + noise

        data = pd.DataFrame(
            {"current_mA": current_sweep, "voltage_mV": voltage_mV}
        )

        client.add_file(
            data,
            tags=[
                user,
                "project:tutorial_resistance",
                f"wafer:{wafer}",
                f"die:{die_id}",
                f"cell:{inst.cell.name}",
                f"device:{inst.name}",
                f"length:{inst.cell.info.length}",
                f"width:{inst.cell.info.width}",
            ],
            filename="iv_measurement.parquet",
        )
  0%|          | 0/8 [00:00<?, ?it/s]
# Save one example measurement locally for use in notebook 3
data.to_parquet("last_measurement.parquet")

plt.plot(data["current_mA"], data["voltage_mV"])
plt.xlabel("Current (mA)")
plt.ylabel("Voltage (mV)")
plt.title(f"Last device: resistance ≈ {resistance:.1f} Ω")
plt.grid(True)
plt.show()

png

What's next?

You have uploaded IV curves for every device on every die across three wafers. The next notebook defines a linear_fit analysis function, wraps it in a pipeline that triggers automatically on new uploads, and runs it on all the files you just uploaded to extract resistance from each IV curve.