Skip to content

layout.layout_pipeline — the layout-to-device workflow

tempura/layout/layout_pipeline.py chains the layout steps into the two convenience functions most users call: prepare_layout(...), which reads, crops, scales, and rasterizes a layout in one call, and build_device(...), which combines the prepared masks with a vertical stack into a finalized Device. The module also holds the small ROI/device summary helpers.

prepare_layout — into internal units

prepare_layout(...) resolves the ROI in physical units, then rescales it so one internal XY cell equals one grid constant. The physical ROI length comes either from the file's unit metadata (size_mode="layout_size") or from an explicit physical_x_length with the \(y\) length inferred from the AOI aspect ratio (size_mode="explicit_x"). The resolved physical ROI must be an integer number of grid cells:

\[ \frac{\ell_\text{ROI}}{\texttt{grid\_constant\_m}} \in \mathbb{Z}. \]

Polygons are scaled independently in \(x\) and \(y\) onto that cell grid and rasterized into the gate_stencils of a PreparedLayout.

After prepare_layout(...), you are in grid units

Tempura stops working in meters once the layout is rasterized. If grid_constant_m = 10e-9, one internal unit is 10 nm, and from then on the AOI size, gate masks, device.length/device.width, layer thicknesses, and every resolution=[dx, dy, dz] are in those units. grid_constant_m is what converts back to meters.

build_device — masks plus a stack

build_device(...) takes the PreparedLayout and an ordered stack specification (bottom-to-top) and returns a finalized device. Each stack entry is a small mapping with a kind of "dielectric", "quantum_region", or "gate", and the relevant physical parameters. A gate entry names the source_layer whose rasterized masks become metal.

A single gate entry can expand into several Gate objects: if its source layer rasterizes to multiple disconnected masks, Tempura emits one gate per connected component and suffixes the names _0, _1, and so on.

A "quantum_region" entry may carry an optional stencil — a boolean mask matching the prepared grid_shape — to pattern the active region into a partial footprint instead of covering the whole ROI:

stack = [
    {"kind": "dielectric", "name": "buffer", "permittivity": 12.9, "thickness": 10.0},
    {
        "kind": "quantum_region",
        "name": "quantum_region",
        "permittivity": 12.9,
        "thickness": 2.0,
        "stencil": active_mask,  # (ny, nx) boolean, matches prepared.grid_shape
    },
    {"kind": "gate", "name": "plunger", "source_layer": "L3D2", "thickness": 2.0},
]

Coplanar batches: {"kind": "batch", ...}

Most stack entries deposit one layer (or one gate that expands into connected components). To deposit several layers at the same height, wrap them in a batch entry whose layers is a list of ordinary stack entries:

{
    "kind": "batch",
    "layers": [
        {"kind": "gate", "name": "left", "source_layer": "L4D2", "thickness": 2.0},
        {"kind": "gate", "name": "right", "source_layer": "L5D2", "thickness": 2.0},
    ],
}

Every layer produced by the batch is deposited coplanar, and their footprints must be disjoint — overlapping footprints raise a ValueError. This mirrors Device.add_layer([...]) at the device level.

Etched openings: inverted=True

Sometimes a layout layer stores the opening in a metal sheet — the hole etched into a global gate — rather than the metal itself. Set inverted=True on the gate entry: Tempura flips the single source stencil so the metal becomes the "on" region, then splits the remaining metal into one gate per disconnected component. This requires exactly one non-empty source stencil to invert, and it applies only to gate layers. (The minimal Kitaev chain example uses this to recover an aluminum gate stored as an opening.)

Summary helpers

format_roi_summary(...) / print_roi_summary(...) and format_device_dimensions(...) / print_device_dimensions(...) render compact text summaries of the prepared ROI and the finalized device for quick checks.

API

layout_pipeline

Shared helpers for layout-backed demo simulations.

build_device(prepared, stack)

Build a finalized device from prepared masks and an ordered stack spec.

The stack describes the physical layers and may optionally include one per-layer resolution entry used directly by build_problem(...). quantum_region entries may also provide boundary_condition to select the linear response model for that region plus a literal stencil for one patterned quantum-region footprint. Gate layers may also set inverted=True when their one source stencil represents an etched opening rather than deposited metal. Layout-backed gate entries expand into one or more concrete Gate objects whose emitted names are suffixed as _0, _1, and so on. One explicit coplanar batch may be expressed as {"kind": "batch", "layers": [...]}.

Parameters:

Name Type Description Default
prepared PreparedLayout

Typed payload returned by :func:prepare_layout.

required
stack list[dict[str, object]]

Ordered layer specification from bottom to top.

required

Returns:

Name Type Description
Finalized Device

class:tempura.electrostatics.device.Device built from the

Device

prepared layout masks and stack specification.

Source code in tempura/layout/layout_pipeline.py
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
def build_device(
    prepared: PreparedLayout,
    stack: list[dict[str, object]],
) -> Device:
    """Build a finalized device from prepared masks and an ordered stack spec.

    The stack describes the physical layers and may optionally include one
    per-layer ``resolution`` entry used directly by ``build_problem(...)``.
    ``quantum_region`` entries may also provide ``boundary_condition`` to select
    the linear response model for that region plus a literal ``stencil`` for one
    patterned quantum-region footprint. Gate layers may also set
    ``inverted=True`` when their one source stencil represents an etched opening
    rather than deposited metal. Layout-backed gate entries expand into one or
    more concrete ``Gate`` objects whose emitted names are suffixed as ``_0``,
    ``_1``, and so on. One explicit coplanar batch may be expressed as
    ``{"kind": "batch", "layers": [...]}``.

    Args:
        prepared: Typed payload returned by :func:`prepare_layout`.
        stack: Ordered layer specification from bottom to top.

    Returns:
        Finalized :class:`tempura.electrostatics.device.Device` built from the
        prepared layout masks and stack specification.
    """
    if not isinstance(prepared, PreparedLayout):
        raise TypeError("build_device() expects a PreparedLayout instance.")

    grid_step = _require_positive(float(prepared.grid_step), name="grid_step")
    roi_length, roi_width = prepared.roi_size
    roi_length = _require_positive(float(roi_length), name="roi_length")
    roi_width = _require_positive(float(roi_width), name="roi_width")
    expected_shape = tuple(int(v) for v in prepared.grid_shape)
    gate_stencils = prepared.gate_stencils

    device = Device(length=roi_length, width=roi_width, grid=grid_step)
    for layer in stack:
        # Stack entries are deliberately plain dictionaries so notebooks and
        # config files can describe the vertical structure without importing
        # every electrostatics class. An explicit ``{"kind": "batch", "layers":
        # [...]}`` entry deposits several coplanar layers together.
        if str(layer["kind"]) == "batch":
            batch_specs = layer.get("layers")
            if not isinstance(batch_specs, list) or not batch_specs:
                raise ValueError("Batch stack entries require a non-empty 'layers' list.")
            batch_layers: list[Gate | Dielectric | QuantumRegion] = []
            for batch_layer in batch_specs:
                if not isinstance(batch_layer, dict):
                    raise TypeError("Batch layers must be mapping entries.")
                batch_layers.extend(
                    _build_stack_layers(
                        batch_layer,
                        expected_shape=expected_shape,
                        gate_stencils=gate_stencils,
                        grid_step=grid_step,
                    )
                )
            _require_disjoint_batch(batch_layers, expected_shape=expected_shape)
            device.add_layer(batch_layers)
            continue

        concrete_layers = _build_stack_layers(
            layer,
            expected_shape=expected_shape,
            gate_stencils=gate_stencils,
            grid_step=grid_step,
        )
        _require_disjoint_batch(concrete_layers, expected_shape=expected_shape)
        if len(concrete_layers) == 1:
            device.add_layer(concrete_layers[0])
        else:
            device.add_layer(concrete_layers)

    device.finalize()
    return device

format_device_dimensions(device, prepared=None)

Return the physical size and XY grid summary for a device.

Parameters:

Name Type Description Default
device Device

Finalized device to summarize.

required
prepared PreparedLayout | None

Optional layout-preparation metadata used to include physical size conversions.

None

Returns:

Type Description
str

Multi-line summary string describing device dimensions, grid shape, and

str

layer names.

Source code in tempura/layout/layout_pipeline.py
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
def format_device_dimensions(
    device: Device,
    prepared: PreparedLayout | None = None,
) -> str:
    """Return the physical size and XY grid summary for a device.

    Args:
        device: Finalized device to summarize.
        prepared: Optional layout-preparation metadata used to include physical
            size conversions.

    Returns:
        Multi-line summary string describing device dimensions, grid shape, and
        layer names.
    """
    ny, nx = tuple(int(v) for v in device.grid_shape)
    summary = {
        "Lx": device.length,
        "Ly": device.width,
        "grid": device.grid,
        "grid_shape": f"(ny={ny}, nx={nx})",
        "grid_points": ny * nx,
        "layers": ", ".join(device.layers),
    }
    if prepared is not None:
        roi_size_m = prepared.roi_size_m
        summary["physical_size"] = (
            f"({_format_meters(roi_size_m[0])}, {_format_meters(roi_size_m[1])})"
        )
        summary["grid_constant"] = _format_meters(prepared.grid_constant_m)
    return format_mapping_block("Device summary", summary)

format_roi_summary(prepared)

Return the ROI size in physical units and grid-normalized units.

Parameters:

Name Type Description Default
prepared PreparedLayout

Typed payload returned by :func:prepare_layout.

required

Returns:

Type Description
str

Multi-line summary string describing ROI size and grid shape.

Source code in tempura/layout/layout_pipeline.py
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
def format_roi_summary(prepared: PreparedLayout) -> str:
    """Return the ROI size in physical units and grid-normalized units.

    Args:
        prepared: Typed payload returned by :func:`prepare_layout`.

    Returns:
        Multi-line summary string describing ROI size and grid shape.
    """
    roi_length_m, roi_width_m = prepared.roi_size_m
    roi_length, roi_width = prepared.roi_size
    ny, nx = tuple(int(v) for v in prepared.grid_shape)
    grid_constant_m = float(prepared.grid_constant_m)

    summary = {
        "physical_size": f"({_format_meters(roi_length_m)}, {_format_meters(roi_width_m)})",
        "grid_constant": _format_meters(grid_constant_m),
        "rescaled_size": f"({roi_length}, {roi_width})",
        "grid_shape": f"(ny={ny}, nx={nx})",
        "grid_points": ny * nx,
    }
    return format_mapping_block("ROI summary", summary)

prepare_layout(layout_path, aoi_bbox, size_mode, grid_constant_m=DEFAULT_GRID_CONSTANT_M, physical_x_length=None, precision=1e-06, tolerance=1e-06)

Load, crop, scale, and rasterize a layout-backed ROI.

The ROI is first resolved in physical units (meters), then rescaled so one internal XY unit corresponds to one grid constant.

Parameters:

Name Type Description Default
layout_path str | Path

Filesystem path to the source layout file.

required
aoi_bbox tuple[float, float, float, float]

AOI bounds in the layout coordinate system.

required
size_mode str

"layout_size" to trust file unit metadata or "explicit_x" to supply the physical x length directly.

required
grid_constant_m float

Physical size represented by one internal XY unit.

DEFAULT_GRID_CONSTANT_M
physical_x_length float | None

Physical x span used when size_mode is "explicit_x".

None
precision float

Boolean-operation precision passed to AOI clipping.

1e-06
tolerance float

Tolerance used when validating that the physical ROI size is an integer multiple of grid_constant_m.

1e-06

Returns:

Type Description
PreparedLayout

Typed layout payload containing the rasterized masks plus the minimum

PreparedLayout

geometry and scaling data needed by :func:build_device.

Source code in tempura/layout/layout_pipeline.py
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
def prepare_layout(
    layout_path: str | Path,
    aoi_bbox: tuple[float, float, float, float],
    size_mode: str,
    grid_constant_m: float = DEFAULT_GRID_CONSTANT_M,
    physical_x_length: float | None = None,
    precision: float = 1e-6,
    tolerance: float = 1e-6,
) -> PreparedLayout:
    """Load, crop, scale, and rasterize a layout-backed ROI.

    The ROI is first resolved in physical units (meters), then rescaled so one
    internal XY unit corresponds to one grid constant.

    Args:
        layout_path: Filesystem path to the source layout file.
        aoi_bbox: AOI bounds in the layout coordinate system.
        size_mode: ``"layout_size"`` to trust file unit metadata or
            ``"explicit_x"`` to supply the physical x length directly.
        grid_constant_m: Physical size represented by one internal XY unit.
        physical_x_length: Physical x span used when ``size_mode`` is
            ``"explicit_x"``.
        precision: Boolean-operation precision passed to AOI clipping.
        tolerance: Tolerance used when validating that the physical ROI size is
            an integer multiple of ``grid_constant_m``.

    Returns:
        Typed layout payload containing the rasterized masks plus the minimum
        geometry and scaling data needed by :func:`build_device`.
    """
    grid_constant_m = _require_positive(grid_constant_m, name="grid_constant_m")

    layout_data = load_layout_data(Path(layout_path))
    polygons_by_layer = layout_data.polygons_by_layer
    # Clip before scaling so the AOI bbox stays in the source file's native
    # coordinate system. The cropped polygons are translated to local AOI
    # coordinates by crop_polygons_to_aoi.
    cropped_polygons_by_layer = crop_polygons_to_aoi(
        polygons_by_layer,
        aoi_bbox,
        precision=precision,
    )

    layout_width = float(aoi_bbox[1] - aoi_bbox[0])
    layout_height = float(aoi_bbox[3] - aoi_bbox[2])
    if layout_width <= 0 or layout_height <= 0:
        raise ValueError("aoi_bbox must define a positive-width, positive-height ROI.")

    if size_mode == "layout_size":
        # Layout-backed sizing trusts metadata from GDS/OAS/DXF. This is the
        # normal path for real layout files with known database units.
        if not layout_data.units_known:
            raise ValueError(
                "size_mode='layout_size' requires layout unit metadata, but none "
                f"was found for {layout_path}."
            )
        if layout_data.unit_scale_m is None:
            raise ValueError(
                "size_mode='layout_size' requires layout unit metadata, but no "
                f"unit scale was found for {layout_path}."
            )
        unit_scale_m = float(layout_data.unit_scale_m)
        roi_length_m = layout_width * unit_scale_m
        roi_width_m = layout_height * unit_scale_m
    elif size_mode == "explicit_x":
        # DXF and other metadata-light sources can still be used when the user
        # supplies one physical span; y follows from the layout aspect ratio.
        if physical_x_length is None:
            raise ValueError("physical_x_length is required for size_mode='explicit_x'.")
        roi_length_m = _require_positive(physical_x_length, name="physical_x_length")
        roi_width_m = roi_length_m * (layout_height / layout_width)
    else:
        raise ValueError(
            "size_mode must be either 'layout_size' or 'explicit_x'; "
            f"got {size_mode!r}."
        )

    roi_length_internal, _nx = _require_grid_multiple(
        roi_length_m,
        grid_constant_m,
        name="roi_length",
        tolerance=tolerance,
    )
    roi_width_internal, _ny = _require_grid_multiple(
        roi_width_m,
        grid_constant_m,
        name="roi_width",
        tolerance=tolerance,
    )

    scaled_cropped_polygons = _scale_polygons(
        cropped_polygons_by_layer,
        sx=roi_length_internal / layout_width,
        sy=roi_width_internal / layout_height,
    )
    # After scaling, one internal coordinate unit equals one grid cell. The
    # rasterizer therefore uses dx=dy=1 and returns masks aligned to Device.grid.
    gate_vertices_by_layer = polygons_to_vertices(scaled_cropped_polygons)
    gate_stencils = rasterize_gate_vertices(
        gate_vertices_by_layer,
        dx=1.0,
        dy=1.0,
        roi_bbox=(0.0, roi_length_internal, 0.0, roi_width_internal),
    )
    return PreparedLayout(
        gate_stencils=gate_stencils,
        roi_size=(roi_length_internal, roi_width_internal),
        grid_constant_m=grid_constant_m,
        cropped_polygons_by_layer=scaled_cropped_polygons,
    )

print_device_dimensions(device, prepared=None)

Print a compact device summary.

Parameters:

Name Type Description Default
device Device

Finalized device to summarize.

required
prepared PreparedLayout | None

Optional layout-preparation metadata used to include physical size conversions.

None

Returns:

Type Description
None

None. The summary is written to standard output.

Source code in tempura/layout/layout_pipeline.py
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
def print_device_dimensions(
    device: Device,
    prepared: PreparedLayout | None = None,
) -> None:
    """Print a compact device summary.

    Args:
        device: Finalized device to summarize.
        prepared: Optional layout-preparation metadata used to include physical
            size conversions.

    Returns:
        ``None``. The summary is written to standard output.
    """
    print(format_device_dimensions(device, prepared))

print_roi_summary(prepared)

Print a compact ROI summary.

Parameters:

Name Type Description Default
prepared PreparedLayout

Typed payload returned by :func:prepare_layout.

required

Returns:

Type Description
None

None. The summary is written to standard output.

Source code in tempura/layout/layout_pipeline.py
592
593
594
595
596
597
598
599
600
601
def print_roi_summary(prepared: PreparedLayout) -> None:
    """Print a compact ROI summary.

    Args:
        prepared: Typed payload returned by :func:`prepare_layout`.

    Returns:
        ``None``. The summary is written to standard output.
    """
    print(format_roi_summary(prepared))

solve_demo(device, vacuum_scale=2, rhs_block_size=1)

Solve the basis problem and extract one rectangular quantum region slice.

Parameters:

Name Type Description Default
device Device

Finalized device to solve.

required
vacuum_scale float

Outer vacuum size multiplier passed to :func:build_problem.

2
rhs_block_size int

Number of gate basis columns solved per linear-system block.

1

Returns:

Type Description
dict[str, object]

Mapping containing gate names, full basis vectors, quantum region plane

dict[str, object]

data, and the finalized problem.

Source code in tempura/layout/layout_pipeline.py
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
def solve_demo(
    device: Device,
    vacuum_scale: float = 2,
    rhs_block_size: int = 1,
) -> dict[str, object]:
    """Solve the basis problem and extract one rectangular quantum region slice.

    Args:
        device: Finalized device to solve.
        vacuum_scale: Outer vacuum size multiplier passed to
            :func:`build_problem`.
        rhs_block_size: Number of gate basis columns solved per linear-system
            block.

    Returns:
        Mapping containing gate names, full basis vectors, quantum region plane
        data, and the finalized problem.
    """
    problem_builder = build_problem(
        device,
        vacuum_scale=vacuum_scale,
    )
    problem = problem_builder.finalized()
    basis_potentials = solve_gate_potentials(
        problem,
        rhs_block_size=rhs_block_size,
    )
    gate_names = [name for name in problem.regions["dirichlet"] if name != "Boundary"]

    quantum_region_layer = device.layers.get("quantum_region")
    plane = extract_quantum_region_plane(problem, problem.regions_shapes)
    # A patterned quantum region does not span the full device footprint, so the
    # extracted plane legitimately covers fewer cells than the device grid.
    if (
        not isinstance(quantum_region_layer, QuantumRegion)
        or quantum_region_layer.stencil is None
    ) and (plane.ny, plane.nx) != tuple(int(v) for v in device.grid_shape):
        raise ValueError(
            "quantum region grid shape mismatch: "
            f"expected {device.grid_shape}, got {(plane.ny, plane.nx)}."
        )

    basis_quantum_region = extract_quantum_region_basis(
        basis_potentials, gate_names, plane
    )

    return {
        "gate_names": gate_names,
        "basis_potentials": basis_potentials,
        "basis_quantum_region": basis_quantum_region,
        "coords_quantum_region": plane.coords,
        "problem": problem,
        "region_shapes": problem.regions_shapes,
    }