Skip to content

electrostatics.height_fields — overhang and height sampling

tempura/electrostatics/height_fields.py holds the small numerical core shared by device finalization: the conformal overhang model for deposited layers and the world-coordinate samplers that read a height array at arbitrary points.

The overhang model

When a layer is deposited over a surface with topography (for example a dielectric poured over a patterned gate), it does not fill only the valleys — it drapes over raised features and climbs their sidewalls. Tempura approximates that draping with a morphological dilation of the surface height map.

For a deposition of thickness \(t\) on an in-plane lattice of spacing \(d_x\), the support base is the local maximum of the surface within a disk of radius

\[ r = \frac{t}{d_x} \quad \text{(in pixels)}, \]

so the base height at cell \((i, j)\) is

\[ \text{base}(i, j) = \max_{(p, q)\,:\,p^2 + q^2 \le r^2} h(i + p,\, j + q), \]

evaluated with scipy.ndimage.maximum_filter over a circular footprint. The deposited top is then \(\text{base} + t\). A larger thickness dilates by a larger disk, which is what makes a thick blanket climb further over a tall gate than a thin one. Tempura currently restricts devices to \(d_x = d_y\), so a single spacing defines the disk.

Sampling height arrays

sample_height_array(...) maps world coordinates \((x, y)\) to array cells using the device extent \(L \times W\) and the array shape \((n_y, n_x)\):

\[ i_x = \left\lfloor \frac{x + L/2}{L / n_x} \right\rfloor, \qquad i_y = \left\lfloor \frac{y + W/2}{W / n_y} \right\rfloor. \]

Points outside the footprint return NaN, so callers can treat them as outside the layer. make_height_fn(...) wraps this as a scalar (x, y) -> height callable used when building backend shapes.

API

height_fields

Height field utilities for layered device geometry.

make_height_fn(arr, L, W)

Create a function that samples a height map in world coordinates.

Assumes the array matches the device XY size and resolution.

Parameters:

Name Type Description Default
arr ndarray

Height values indexed as (row=y, col=x).

required
L float

Device length (x extent).

required
W float

Device width (y extent).

required

Returns:

Type Description
Callable[[float, float], float]

Callable that maps (x, y) -> height.

Source code in tempura/electrostatics/height_fields.py
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
def make_height_fn(
    arr: np.ndarray, L: float, W: float
) -> Callable[[float, float], float]:
    """Create a function that samples a height map in world coordinates.

    Assumes the array matches the device XY size and resolution.

    Args:
        arr: Height values indexed as (row=y, col=x).
        L: Device length (x extent).
        W: Device width (y extent).

    Returns:
        Callable that maps (x, y) -> height.
    """

    def _fn(x: float, y: float) -> float:
        """Sample the height array at the cell containing ``(x, y)``."""
        return float(sample_height_array(arr, L, W, x, y).item())

    return _fn

overhang_base(height_map, *, thickness, dx)

Return the support surface produced by one deposition step.

The overhang model is shared by the generic height-field utility and the full Device finalization path so both use the same stencil-expansion behavior. Tempura currently restricts devices to dx == dy, so the single dx spacing is the common in-plane lattice constant.

Source code in tempura/electrostatics/height_fields.py
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
def overhang_base(height_map: np.ndarray, *, thickness: float, dx: float) -> np.ndarray:
    """Return the support surface produced by one deposition step.

    The overhang model is shared by the generic height-field utility and the
    full ``Device`` finalization path so both use the same stencil-expansion
    behavior. Tempura currently restricts devices to ``dx == dy``, so the
    single ``dx`` spacing is the common in-plane lattice constant.
    """
    radius_pixels = float(thickness) / float(dx)
    if radius_pixels <= 0.0:
        return height_map

    radius_int = int(np.ceil(radius_pixels))
    yy, xx = np.ogrid[-radius_int : radius_int + 1, -radius_int : radius_int + 1]
    footprint = (xx * xx + yy * yy) <= (radius_pixels * radius_pixels)
    if not np.any(footprint):
        return height_map
    return maximum_filter(height_map, footprint=footprint, mode="nearest")

sample_height_array(arr, L, W, x, y)

Sample one height array at vectorized world coordinates.

Points outside the device footprint are returned as NaN so callers can treat them as outside the layer.

Source code in tempura/electrostatics/height_fields.py
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
def sample_height_array(
    arr: np.ndarray,
    L: float,
    W: float,
    x: np.ndarray | float,
    y: np.ndarray | float,
) -> np.ndarray:
    """Sample one height array at vectorized world coordinates.

    Points outside the device footprint are returned as ``NaN`` so callers can
    treat them as outside the layer.
    """
    arr = np.asarray(arr, dtype=float)
    if arr.shape[1] == 0 or arr.shape[0] == 0:
        raise ValueError("Height array must be non-empty.")

    x_arr, y_arr = np.broadcast_arrays(
        np.asarray(x, dtype=float), np.asarray(y, dtype=float)
    )
    sampled = np.full(x_arr.shape, np.nan, dtype=float)

    dx = L / float(arr.shape[1])
    dy = W / float(arr.shape[0])
    half_L = L / 2.0
    half_W = W / 2.0

    ix = np.floor((x_arr + half_L) / dx).astype(int)
    iy = np.floor((y_arr + half_W) / dy).astype(int)
    valid = (ix >= 0) & (iy >= 0) & (iy < arr.shape[0]) & (ix < arr.shape[1])
    if np.any(valid):
        sampled[valid] = arr[iy[valid], ix[valid]]
    return sampled