Skip to content

Visualization

Paper-ready matplotlib helpers for IEEE / SPIE publication. Requires the [jupyter] extra (matplotlib).

openlithohub.vis.contours

Paper-ready contour overlays for OPC / ILT / inverse-lithography results.

plot_contours(target, predicted, *, pv_band=None, pixel_size_nm=1.0, title=None, style='ieee', save_path=None, show_legend=True)

Render a target / predicted contour overlay with optional PV band.

Produces a single-panel figure suitable for direct inclusion in IEEE / SPIE papers. Contours are extracted with matplotlib.contour at the 0.5 iso-level so the figure remains crisp at any DPI.

Parameters:

Name Type Description Default
target Tensor | ndarray

Target binary mask (H, W).

required
predicted Tensor | ndarray

Predicted / simulated binary mask (H, W).

required
pv_band Tensor | ndarray | None

Optional PV-band mask where > 0 marks band region.

None
pixel_size_nm float

Physical pixel pitch (axes are labelled in nm).

1.0
title str | None

Optional figure title.

None
style str

"ieee" or "spie".

'ieee'
save_path str | Path | None

If given, fig.savefig is called (vector format inferred from extension; .pdf recommended for camera-ready).

None
show_legend bool

Toggle legend rendering.

True

Returns:

Type Description
Any

The matplotlib Figure object.

Source code in src/openlithohub/vis/contours.py
def plot_contours(
    target: torch.Tensor | np.ndarray,
    predicted: torch.Tensor | np.ndarray,
    *,
    pv_band: torch.Tensor | np.ndarray | None = None,
    pixel_size_nm: float = 1.0,
    title: str | None = None,
    style: str = "ieee",
    save_path: str | Path | None = None,
    show_legend: bool = True,
) -> Any:
    """Render a target / predicted contour overlay with optional PV band.

    Produces a single-panel figure suitable for direct inclusion in IEEE / SPIE
    papers. Contours are extracted with ``matplotlib.contour`` at the 0.5
    iso-level so the figure remains crisp at any DPI.

    Args:
        target: Target binary mask (H, W).
        predicted: Predicted / simulated binary mask (H, W).
        pv_band: Optional PV-band mask where ``> 0`` marks band region.
        pixel_size_nm: Physical pixel pitch (axes are labelled in nm).
        title: Optional figure title.
        style: ``"ieee"`` or ``"spie"``.
        save_path: If given, ``fig.savefig`` is called (vector format inferred
            from extension; ``.pdf`` recommended for camera-ready).
        show_legend: Toggle legend rendering.

    Returns:
        The matplotlib ``Figure`` object.
    """
    tgt = ensure_2d(torch.as_tensor(_to_numpy(target)))
    pred = ensure_2d(torch.as_tensor(_to_numpy(predicted)))
    tgt_arr = _to_numpy(tgt).astype(np.float32)
    pred_arr = _to_numpy(pred).astype(np.float32)

    h, w = tgt_arr.shape
    extent = (0.0, w * pixel_size_nm, 0.0, h * pixel_size_nm)

    with paper_style(style):
        import matplotlib.pyplot as plt

        fig, ax = plt.subplots(1, 1)
        ax.imshow(
            np.ones_like(tgt_arr),
            cmap="gray",
            vmin=0,
            vmax=1,
            extent=extent,
            origin="lower",
            interpolation="nearest",
        )

        if pv_band is not None:
            band_arr = _to_numpy(pv_band).astype(np.float32)
            ax.contourf(
                band_arr,
                levels=[0.5, 1.5],
                colors=[PALETTE["pv_outer"]],
                alpha=0.35,
                extent=extent,
                origin="lower",
            )

        ax.contour(
            tgt_arr,
            levels=[0.5],
            colors=[PALETTE["target"]],
            linewidths=1.0,
            extent=extent,
            origin="lower",
        )
        ax.contour(
            pred_arr,
            levels=[0.5],
            colors=[PALETTE["predicted"]],
            linewidths=1.0,
            linestyles="--",
            extent=extent,
            origin="lower",
        )

        ax.set_xlabel("x (nm)")
        ax.set_ylabel("y (nm)")
        ax.set_aspect("equal")
        if title:
            ax.set_title(title)

        if show_legend:
            from matplotlib.lines import Line2D
            from matplotlib.patches import Patch

            handles: list[Any] = [
                Line2D([0], [0], color=PALETTE["target"], lw=1.0, label="Target"),
                Line2D([0], [0], color=PALETTE["predicted"], lw=1.0, ls="--", label="Predicted"),
            ]
            if pv_band is not None:
                handles.append(Patch(facecolor=PALETTE["pv_outer"], alpha=0.35, label="PV band"))
            ax.legend(handles=handles, loc="upper right", frameon=False)

        fig.tight_layout()

        if save_path is not None:
            out = Path(save_path)
            out.parent.mkdir(parents=True, exist_ok=True)
            fig.savefig(out)

    return fig

plot_pv_band(nominal, inner, outer, *, pixel_size_nm=1.0, title=None, style='ieee', save_path=None)

Render an inner / nominal / outer PV-band envelope figure.

Shows the inner (intersection) and outer (union) resist contours bracketing the nominal contour — the standard lithography PV-band figure.

Source code in src/openlithohub/vis/contours.py
def plot_pv_band(
    nominal: torch.Tensor | np.ndarray,
    inner: torch.Tensor | np.ndarray,
    outer: torch.Tensor | np.ndarray,
    *,
    pixel_size_nm: float = 1.0,
    title: str | None = None,
    style: str = "ieee",
    save_path: str | Path | None = None,
) -> Any:
    """Render an inner / nominal / outer PV-band envelope figure.

    Shows the inner (intersection) and outer (union) resist contours bracketing
    the nominal contour — the standard lithography PV-band figure.
    """
    nom_arr = _to_numpy(nominal).astype(np.float32)
    inn_arr = _to_numpy(inner).astype(np.float32)
    out_arr = _to_numpy(outer).astype(np.float32)

    h, w = nom_arr.shape
    extent = (0.0, w * pixel_size_nm, 0.0, h * pixel_size_nm)

    with paper_style(style):
        import matplotlib.pyplot as plt

        fig, ax = plt.subplots(1, 1)
        ax.imshow(
            np.ones_like(nom_arr),
            cmap="gray",
            vmin=0,
            vmax=1,
            extent=extent,
            origin="lower",
            interpolation="nearest",
        )

        band = np.clip(out_arr - inn_arr, 0.0, 1.0)
        ax.contourf(
            band,
            levels=[0.5, 1.5],
            colors=[PALETTE["pv_outer"]],
            alpha=0.35,
            extent=extent,
            origin="lower",
        )
        ax.contour(
            nom_arr,
            levels=[0.5],
            colors=["black"],
            linewidths=1.0,
            extent=extent,
            origin="lower",
        )
        ax.contour(
            inn_arr,
            levels=[0.5],
            colors=[PALETTE["pv_inner"]],
            linewidths=0.7,
            extent=extent,
            origin="lower",
        )
        ax.contour(
            out_arr,
            levels=[0.5],
            colors=[PALETTE["predicted"]],
            linewidths=0.7,
            extent=extent,
            origin="lower",
        )

        ax.set_xlabel("x (nm)")
        ax.set_ylabel("y (nm)")
        ax.set_aspect("equal")
        if title:
            ax.set_title(title)

        from matplotlib.lines import Line2D
        from matplotlib.patches import Patch

        handles = [
            Line2D([0], [0], color="black", lw=1.0, label="Nominal"),
            Line2D([0], [0], color=PALETTE["pv_inner"], lw=0.7, label="Inner"),
            Line2D([0], [0], color=PALETTE["predicted"], lw=0.7, label="Outer"),
            Patch(facecolor=PALETTE["pv_outer"], alpha=0.35, label="PV band"),
        ]
        ax.legend(handles=handles, loc="upper right", frameon=False)

        fig.tight_layout()
        if save_path is not None:
            out = Path(save_path)
            out.parent.mkdir(parents=True, exist_ok=True)
            fig.savefig(out)

    return fig

openlithohub.vis.style

Paper-publication matplotlib style presets.

Two presets are provided:

  • IEEE_STYLE — IEEE two-column papers (column width ≈ 3.5 in), serif fonts, 600 dpi raster fallback, vector PDF preferred.
  • SPIE_STYLE — SPIE Advanced Lithography proceedings (single column ≈ 6.5 in), Helvetica-like sans, 600 dpi.

Use via the paper_style context manager so global rcParams are restored.

paper_style(style='ieee')

Temporarily apply a paper-publication matplotlib style.

Parameters:

Name Type Description Default
style str | dict[str, Any]

"ieee", "spie", or a custom rcParams dict.

'ieee'
Source code in src/openlithohub/vis/style.py
@contextmanager
def paper_style(style: str | dict[str, Any] = "ieee") -> Iterator[None]:
    """Temporarily apply a paper-publication matplotlib style.

    Args:
        style: ``"ieee"``, ``"spie"``, or a custom rcParams dict.
    """
    try:
        import matplotlib.pyplot as plt
    except ImportError as exc:
        raise ImportError(
            "matplotlib is required for openlithohub.vis. "
            "Install with: pip install openlithohub[jupyter]"
        ) from exc

    if isinstance(style, str):
        if style.lower() == "ieee":
            rc = IEEE_STYLE
        elif style.lower() == "spie":
            rc = SPIE_STYLE
        else:
            raise ValueError(f"unknown style '{style}', expected 'ieee' or 'spie'")
    else:
        rc = style

    with plt.rc_context(rc):
        yield