Skip to content

tempura — the package root

tempura/__init__.py is the happy-path import for the core workflow. It re-exports the handful of names you need to go from a layout to a solved potential, so most scripts import everything from the top level.

The curated surface

from tempura import (
    Device,
    Dielectric,
    Gate,
    QuantumRegion,
    prepare_layout,
    build_device,
    build_problem,
    solve_gate_potentials,
)

These cover the whole path: describe a stack (Device, Gate, Dielectric, QuantumRegion), turn a layout file into masks and a device (prepare_layout, build_device), and assemble and solve the Poisson problem (build_problem, solve_gate_potentials). The post-solve helpers extract_quantum_region_plane(...) and extract_quantum_region_basis(...) live one level down in tempura.electrostatics.

The root package is intentionally small. Everything importable from tempura is meant to be stable; the per-module pages in this reference document the internal machinery behind these names for contributors and curious users.

API

tempura

Public package API for Tempura.

Device

Ordered device stack used to derive solver geometry.

A device stores layers in bottom-to-top deposition order. Gates may be passed as a list to :meth:add_layer when they are deposited in one metal batch; non-gate layers cover the full device footprint.

Parameters:

Name Type Description Default
length float

Device span along x in internal units.

required
width float

Device span along y in internal units.

required
grid float

Square in-plane lattice spacing. length and width must be integer multiples of grid.

required

Attributes:

Name Type Description
length

Device span along x.

width

Device span along y.

grid_shape

Raster shape as (ny, nx).

Source code in tempura/electrostatics/device.py
593
594
595
596
597
598
599
600
601
602
603
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
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
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
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
class Device:
    """Ordered device stack used to derive solver geometry.

    A device stores layers in bottom-to-top deposition order. Gates may be
    passed as a list to :meth:`add_layer` when they are deposited in one metal
    batch; non-gate layers cover the full device footprint.

    Args:
        length: Device span along x in internal units.
        width: Device span along y in internal units.
        grid: Square in-plane lattice spacing. ``length`` and ``width`` must be
            integer multiples of ``grid``.

    Attributes:
        length: Device span along x.
        width: Device span along y.
        grid_shape: Raster shape as ``(ny, nx)``.
    """

    def __init__(
        self,
        *,
        length: float,
        width: float,
        grid: float,
    ):
        """Create a device with explicit dimensions and base XY lattice spacing.

        Args:
            length: Device span along x in internal units.
            width: Device span along y in internal units.
            grid: Square in-plane grid spacing.

        Raises:
            ValueError: If dimensions are non-positive or not commensurate with
                ``grid``.
            TypeError: If ``grid`` is omitted.
        """
        self.length = _require_positive(length, name="length")
        self.width = _require_positive(width, name="width")
        self._grid = _resolve_grid_spacing(grid=grid)
        self.grid_shape = (
            _grid_count(self.width, self.grid, span_name="width", step_name="grid"),
            _grid_count(self.length, self.grid, span_name="length", step_name="grid"),
        )
        self._layers: dict[str, Layer] = {}
        self._deposition_batches: list[tuple[str, ...]] = []
        self._finalized = False

    @property
    def grid(self) -> float:
        """Return the square in-plane lattice spacing."""
        return float(self._grid)

    @property
    def finalized(self) -> bool:
        """Return whether :meth:`finalize` has been called successfully."""
        return self._finalized

    @property
    def layers(self) -> Mapping[str, Layer]:
        """Return the ordered layer stack as a read-only mapping."""
        return MappingProxyType(self._layers)

    def add_layer(self, layer: LayerInput) -> None:
        """Add a layer in bottom-to-top stacking order.

        Args:
            layer: Either one layer instance or one non-empty list of layers
                deposited together in one coplanar batch.

        Raises:
            RuntimeError: If the device has already been finalized.
            ValueError: If names collide, a layer footprint shape is invalid, or
                one batch contains overlapping footprints. ``"Boundary"`` is
                reserved for Tempura's generated grounded boundary region.
            TypeError: If ``layer`` is not one supported layer input.
        """
        if self._finalized:
            raise RuntimeError("Cannot add layers after device.finalize().")
        batch = self._normalize_deposition_batch(layer)

        names_in_batch: set[str] = set()
        resolved_resolutions: dict[int, np.ndarray] = {}
        # Layers deposited together in one batch occupy the same height, so their
        # footprints must be disjoint to remain physically unambiguous.
        occupied = np.zeros(self.grid_shape, dtype=bool)
        for batch_layer in batch:
            # Layer names become solver-region names, so validate all naming
            # constraints before mutating the ordered device state.
            if batch_layer.name == "Boundary":
                raise ValueError(
                    "Layer name 'Boundary' is reserved for Tempura's grounded "
                    "outer Dirichlet boundary."
                )
            if batch_layer.name in self._layers or batch_layer.name in names_in_batch:
                raise ValueError(
                    f"Layer name {batch_layer.name!r} is already present in the device."
                )
            names_in_batch.add(batch_layer.name)

            if batch_layer.resolution is None:
                # Default resolutions depend on device.grid and layer thickness.
                # Resolve them before insertion so every stored layer has an
                # explicit mesh spacing by the end of this method.
                resolved_resolutions[id(batch_layer)] = (
                    self._default_resolution_for_layer(batch_layer)
                )

            mask = self._layer_mask(batch_layer)
            if mask.shape != self.grid_shape:
                raise ValueError(
                    f"{batch_layer.name} footprint shape must match device "
                    f"grid_shape {self.grid_shape}; got {mask.shape}."
                )
            if np.any(occupied & mask):
                overlapping_names = sorted(names_in_batch)
                raise ValueError(
                    f"Layer batch {overlapping_names} has overlapping footprints."
                )
            occupied |= mask

        for batch_layer in batch:
            # Normalize user-supplied and default resolutions through the same
            # device-grid rules, then attach the normalized array to the layer
            # object that will be finalized later.
            requested_resolution = (
                resolved_resolutions[id(batch_layer)]
                if batch_layer.resolution is None
                else batch_layer.resolution
            )
            resolution = _normalize_resolution_against_device_grid(
                requested_resolution,
                grid=self.grid,
                name=f"{batch_layer.name}.resolution",
                warn_on_z_rounding=batch_layer.resolution is not None,
            )
            object.__setattr__(
                batch_layer,
                "resolution",
                np.asarray(resolution, dtype=float),
            )
            self._layers[batch_layer.name] = batch_layer

        self._deposition_batches.append(tuple(batch_layer.name for batch_layer in batch))

    def finalize(self) -> None:
        """Realize all layer height fields and freeze the device.

        Finalization converts the ordered layer specifications into concrete
        bottom/top height arrays, Pescado-compatible shapes, and read-only layer
        buffers. Calling it more than once is a no-op.

        Raises:
            RuntimeError: If the internal finalization pipeline cannot produce
                geometry for a registered layer.
        """
        if self._finalized:
            return

        self._validate_thickness_commensurability()
        deposition_plan = self._build_deposition_plan()
        finalization = self._realize_finalized_geometry(deposition_plan)
        self._apply_finalized_geometry(finalization)
        self._freeze_layers()
        self._finalized = True

    def _validate_thickness_commensurability(self) -> None:
        """Require every layer thickness to be an integer multiple of the Z lattice.

        The quantum-region ``dz`` is the single anchor for the Z mesh lattice:
        mesh planes sit at ``z = k * dz`` from the stack bottom. Layer interfaces
        are placed at the cumulative sum of thicknesses, so unless each thickness
        is an integer multiple of ``dz`` the interfaces drift off the lattice
        planes. That makes the shared-face exclusion ambiguous and can leave a
        refinement zone owning zero mesh sites, surfacing only as a late, opaque
        error inside ``build_problem()``. Validating here fails early, naming the
        offending layer.

        Skipped when the device has no :class:`QuantumRegion` yet; that missing
        anchor is reported later by the mesh planner with its own clear error.

        Raises:
            ValueError: If a layer thickness is not an integer multiple of the
                quantum-region ``dz``.
        """
        quantum_regions = [
            layer for layer in self._layers.values() if isinstance(layer, QuantumRegion)
        ]
        if not quantum_regions:
            return
        master_dz = float(np.asarray(quantum_regions[0].resolution, dtype=float)[2])
        for layer in self._layers.values():
            ratio = float(layer.thickness) / master_dz
            if not np.isclose(ratio, round(ratio), atol=1e-9, rtol=0.0):
                raise ValueError(
                    f"{layer.name}.thickness={float(layer.thickness)} must be an "
                    f"integer multiple of the quantum-region z spacing dz="
                    f"{master_dz} so layer interfaces land on mesh planes."
                )

    def _default_resolution_for_layer(self, layer: Layer) -> np.ndarray:
        """Return the default mesh resolution for one layer."""
        return _normalize_resolution_against_device_grid(
            np.array(
                [
                    float(self.grid),
                    float(self.grid),
                    min(float(self.grid), float(layer.thickness)),
                ],
                dtype=float,
            ),
            grid=self.grid,
            name=f"{layer.name}.resolution",
            warn_on_z_rounding=False,
        )

    def _normalize_deposition_batch(self, layer: LayerInput) -> list[Layer]:
        """Return one validated deposition batch from ``add_layer(...)`` input."""
        if isinstance(layer, list):
            if not layer:
                raise ValueError("Layer batches passed to add_layer() must be non-empty.")
            if not all(
                isinstance(batch_layer, (Gate, Dielectric, QuantumRegion))
                for batch_layer in layer
            ):
                raise TypeError(
                    "Layer batches passed to add_layer() must contain only Device layers."
                )
            return list(layer)

        if not isinstance(layer, (Gate, Dielectric, QuantumRegion)):
            raise TypeError(
                "add_layer() expects a Gate, Dielectric, QuantumRegion, or one "
                "non-empty list of layers."
            )
        return [layer]

    def _iter_deposition_batches(self) -> Iterator[list[Layer]]:
        """Yield layers grouped by deposition step."""
        for batch_names in self._deposition_batches:
            yield [self._layers[name] for name in batch_names]

    def _build_deposition_plan(self) -> tuple[DepositionBatch, ...]:
        """Return ordered deposition batches.

        Returns:
            Tuple of layer batches used by the shared finalization pipeline.
        """
        return tuple(tuple(batch) for batch in self._iter_deposition_batches())

    def _realize_finalized_geometry(
        self,
        deposition_plan: tuple[DepositionBatch, ...],
    ) -> DeviceFinalizationArtifacts:
        """Realize height arrays and backend shapes from one shared model.

        Args:
            deposition_plan: Ordered deposition batches.

        Returns:
            Mapping of layer name to finalized geometry information produced by
            one shared surface-evolution model.
        """
        surface = np.zeros(self.grid_shape, dtype=float)
        current_z0 = 0.0
        layer_geometries: DeviceFinalizationArtifacts = {}

        for layers in deposition_plan:
            batch_geometries, surface, current_z0 = self._realize_batch(
                layers,
                surface=surface,
                current_z0=current_z0,
            )
            layer_geometries.update(batch_geometries)

        return layer_geometries

    def _realize_batch(
        self,
        layers: tuple[Layer, ...],
        *,
        surface: np.ndarray,
        current_z0: float,
    ) -> tuple[dict[str, FinalizedLayerGeometry], np.ndarray, float]:
        """Realize one coplanar deposition batch from the shared surface state.

        Args:
            layers: Objects deposited together in one batch.
            surface: Running top surface before the batch is deposited.
            current_z0: Running fallback z origin before the batch is
                deposited.

        Returns:
            Tuple of ``(geometries, next_surface, next_z0)`` for the realized
            batch.
        """
        h_before = surface.copy()
        geometries: dict[str, FinalizedLayerGeometry] = {}
        # Every layer in the batch starts from the same pre-deposition surface
        # and advances the running surface to the tallest realized top at each
        # cell. Gates deposit flat onto the surface; covering layers overhang
        # over nearby lower cells per the shared height-field convention.
        next_surface = h_before.copy()
        for layer in layers:
            mask = self._layer_mask(layer)
            z_bot = np.full_like(h_before, np.nan, dtype=float)
            z_top = np.full_like(h_before, np.nan, dtype=float)
            if isinstance(layer, Gate):
                z_bot[mask] = h_before[mask]
                z_top[mask] = h_before[mask] + float(layer.thickness)
            else:
                thickness = float(layer.thickness)
                base = overhang_base(h_before, thickness=thickness, dx=self.grid)
                z_bot[mask] = h_before[mask]
                z_top[mask] = base[mask] + thickness
            geometry = _build_finalized_layer_geometry(
                layer,
                z_bot,
                z_top,
                device_length=self.length,
                device_width=self.width,
                fallback_z0=current_z0,
            )
            geometries[layer.name] = geometry
            next_surface = np.maximum(
                next_surface,
                np.nan_to_num(cast(np.ndarray, geometry["z_top_arr"]), nan=-np.inf),
            )

        next_z0 = max(
            float(cast(float, geometry["top_z"])) for geometry in geometries.values()
        )
        return geometries, next_surface, next_z0

    def _apply_finalized_geometry(
        self,
        finalization: DeviceFinalizationArtifacts,
    ) -> None:
        """Attach realized geometry mappings to the user-facing layer specs."""
        for name, layer in self._layers.items():
            try:
                layer._set_finalized_geometry(finalization[name])
            except KeyError as exc:
                raise RuntimeError(
                    f"Missing finalized geometry for layer {name!r}."
                ) from exc

    def _freeze_layers(self) -> None:
        """Freeze all layers after the full geometry pipeline succeeds."""
        for layer in self._layers.values():
            layer._freeze()

    def _layer_mask(self, layer: Layer) -> np.ndarray:
        """Return the mask for a layer, defaulting to full coverage."""
        if isinstance(layer, Gate):
            return layer.stencil
        if isinstance(layer, QuantumRegion) and layer.stencil is not None:
            return layer.stencil
        return np.ones(self.grid_shape, dtype=bool)
finalized property

Return whether :meth:finalize has been called successfully.

grid property

Return the square in-plane lattice spacing.

layers property

Return the ordered layer stack as a read-only mapping.

__init__(*, length, width, grid)

Create a device with explicit dimensions and base XY lattice spacing.

Parameters:

Name Type Description Default
length float

Device span along x in internal units.

required
width float

Device span along y in internal units.

required
grid float

Square in-plane grid spacing.

required

Raises:

Type Description
ValueError

If dimensions are non-positive or not commensurate with grid.

TypeError

If grid is omitted.

Source code in tempura/electrostatics/device.py
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
def __init__(
    self,
    *,
    length: float,
    width: float,
    grid: float,
):
    """Create a device with explicit dimensions and base XY lattice spacing.

    Args:
        length: Device span along x in internal units.
        width: Device span along y in internal units.
        grid: Square in-plane grid spacing.

    Raises:
        ValueError: If dimensions are non-positive or not commensurate with
            ``grid``.
        TypeError: If ``grid`` is omitted.
    """
    self.length = _require_positive(length, name="length")
    self.width = _require_positive(width, name="width")
    self._grid = _resolve_grid_spacing(grid=grid)
    self.grid_shape = (
        _grid_count(self.width, self.grid, span_name="width", step_name="grid"),
        _grid_count(self.length, self.grid, span_name="length", step_name="grid"),
    )
    self._layers: dict[str, Layer] = {}
    self._deposition_batches: list[tuple[str, ...]] = []
    self._finalized = False
add_layer(layer)

Add a layer in bottom-to-top stacking order.

Parameters:

Name Type Description Default
layer LayerInput

Either one layer instance or one non-empty list of layers deposited together in one coplanar batch.

required

Raises:

Type Description
RuntimeError

If the device has already been finalized.

ValueError

If names collide, a layer footprint shape is invalid, or one batch contains overlapping footprints. "Boundary" is reserved for Tempura's generated grounded boundary region.

TypeError

If layer is not one supported layer input.

Source code in tempura/electrostatics/device.py
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
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
def add_layer(self, layer: LayerInput) -> None:
    """Add a layer in bottom-to-top stacking order.

    Args:
        layer: Either one layer instance or one non-empty list of layers
            deposited together in one coplanar batch.

    Raises:
        RuntimeError: If the device has already been finalized.
        ValueError: If names collide, a layer footprint shape is invalid, or
            one batch contains overlapping footprints. ``"Boundary"`` is
            reserved for Tempura's generated grounded boundary region.
        TypeError: If ``layer`` is not one supported layer input.
    """
    if self._finalized:
        raise RuntimeError("Cannot add layers after device.finalize().")
    batch = self._normalize_deposition_batch(layer)

    names_in_batch: set[str] = set()
    resolved_resolutions: dict[int, np.ndarray] = {}
    # Layers deposited together in one batch occupy the same height, so their
    # footprints must be disjoint to remain physically unambiguous.
    occupied = np.zeros(self.grid_shape, dtype=bool)
    for batch_layer in batch:
        # Layer names become solver-region names, so validate all naming
        # constraints before mutating the ordered device state.
        if batch_layer.name == "Boundary":
            raise ValueError(
                "Layer name 'Boundary' is reserved for Tempura's grounded "
                "outer Dirichlet boundary."
            )
        if batch_layer.name in self._layers or batch_layer.name in names_in_batch:
            raise ValueError(
                f"Layer name {batch_layer.name!r} is already present in the device."
            )
        names_in_batch.add(batch_layer.name)

        if batch_layer.resolution is None:
            # Default resolutions depend on device.grid and layer thickness.
            # Resolve them before insertion so every stored layer has an
            # explicit mesh spacing by the end of this method.
            resolved_resolutions[id(batch_layer)] = (
                self._default_resolution_for_layer(batch_layer)
            )

        mask = self._layer_mask(batch_layer)
        if mask.shape != self.grid_shape:
            raise ValueError(
                f"{batch_layer.name} footprint shape must match device "
                f"grid_shape {self.grid_shape}; got {mask.shape}."
            )
        if np.any(occupied & mask):
            overlapping_names = sorted(names_in_batch)
            raise ValueError(
                f"Layer batch {overlapping_names} has overlapping footprints."
            )
        occupied |= mask

    for batch_layer in batch:
        # Normalize user-supplied and default resolutions through the same
        # device-grid rules, then attach the normalized array to the layer
        # object that will be finalized later.
        requested_resolution = (
            resolved_resolutions[id(batch_layer)]
            if batch_layer.resolution is None
            else batch_layer.resolution
        )
        resolution = _normalize_resolution_against_device_grid(
            requested_resolution,
            grid=self.grid,
            name=f"{batch_layer.name}.resolution",
            warn_on_z_rounding=batch_layer.resolution is not None,
        )
        object.__setattr__(
            batch_layer,
            "resolution",
            np.asarray(resolution, dtype=float),
        )
        self._layers[batch_layer.name] = batch_layer

    self._deposition_batches.append(tuple(batch_layer.name for batch_layer in batch))
finalize()

Realize all layer height fields and freeze the device.

Finalization converts the ordered layer specifications into concrete bottom/top height arrays, Pescado-compatible shapes, and read-only layer buffers. Calling it more than once is a no-op.

Raises:

Type Description
RuntimeError

If the internal finalization pipeline cannot produce geometry for a registered layer.

Source code in tempura/electrostatics/device.py
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
def finalize(self) -> None:
    """Realize all layer height fields and freeze the device.

    Finalization converts the ordered layer specifications into concrete
    bottom/top height arrays, Pescado-compatible shapes, and read-only layer
    buffers. Calling it more than once is a no-op.

    Raises:
        RuntimeError: If the internal finalization pipeline cannot produce
            geometry for a registered layer.
    """
    if self._finalized:
        return

    self._validate_thickness_commensurability()
    deposition_plan = self._build_deposition_plan()
    finalization = self._realize_finalized_geometry(deposition_plan)
    self._apply_finalized_geometry(finalization)
    self._freeze_layers()
    self._finalized = True

Dielectric dataclass

Bases: LayerBase

Dielectric layer covering the full XY device footprint.

Parameters:

Name Type Description Default
name str

Unique dielectric name.

required
thickness float

Dielectric thickness in device units.

required
permittivity float

Relative permittivity assigned to the layer.

required
resolution ndarray | None

Optional [dx, dy, dz] mesh spacing override.

None

Raises:

Type Description
ValueError

If thickness or permittivity is not positive.

Source code in tempura/electrostatics/device.py
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
@dataclass
class Dielectric(LayerBase):
    """Dielectric layer covering the full XY device footprint.

    Args:
        name: Unique dielectric name.
        thickness: Dielectric thickness in device units.
        permittivity: Relative permittivity assigned to the layer.
        resolution: Optional ``[dx, dy, dz]`` mesh spacing override.

    Raises:
        ValueError: If ``thickness`` or ``permittivity`` is not positive.
    """

    permittivity: float

    def __post_init__(self) -> None:
        """Validate and normalize the dielectric permittivity."""
        super().__post_init__()
        object.__setattr__(
            self,
            "permittivity",
            _require_positive(self.permittivity, name=f"{self.name}.permittivity"),
        )
__post_init__()

Validate and normalize the dielectric permittivity.

Source code in tempura/electrostatics/device.py
518
519
520
521
522
523
524
525
def __post_init__(self) -> None:
    """Validate and normalize the dielectric permittivity."""
    super().__post_init__()
    object.__setattr__(
        self,
        "permittivity",
        _require_positive(self.permittivity, name=f"{self.name}.permittivity"),
    )

Gate dataclass

Bases: LayerBase

Metallic gate layer described by a device-aligned stencil.

Parameters:

Name Type Description Default
name str

Unique gate name. The name becomes the Dirichlet region name and the key returned by :func:solve_gate_potentials.

required
thickness float

Gate thickness in device units.

required
stencil ndarray

Boolean (ny, nx) mask aligned to Device.grid_shape. True cells contain metal.

required
Note

Gates have no refinement zone of their own: they are realized on the device-halo lattice. Passing resolution is therefore rejected; a warning is emitted at build time if a stencil feature is finer than the realizing mesh.

Raises:

Type Description
ValueError

If stencil is missing, is not 2D, contains no active cells, or resolution is supplied.

Source code in tempura/electrostatics/device.py
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
@dataclass
class Gate(LayerBase):
    """Metallic gate layer described by a device-aligned stencil.

    Args:
        name: Unique gate name. The name becomes the Dirichlet region name and
            the key returned by :func:`solve_gate_potentials`.
        thickness: Gate thickness in device units.
        stencil: Boolean ``(ny, nx)`` mask aligned to ``Device.grid_shape``.
            ``True`` cells contain metal.

    Note:
        Gates have no refinement zone of their own: they are realized on the
        device-halo lattice. Passing ``resolution`` is therefore rejected; a
        warning is emitted at build time if a stencil feature is finer than the
        realizing mesh.

    Raises:
        ValueError: If ``stencil`` is missing, is not 2D, contains no active
            cells, or ``resolution`` is supplied.
    """

    stencil: np.ndarray

    def __post_init__(self) -> None:
        """Validate the gate stencil and normalize it to a boolean mask."""
        super().__post_init__()
        if self.resolution is not None:
            raise ValueError(
                f"{self.name}.resolution is not supported: gates have no "
                "refinement zone of their own and inherit the device-halo "
                "lattice. Remove the resolution argument."
            )
        if self.stencil is None:
            raise ValueError("Gate requires a stencil.")
        object.__setattr__(
            self,
            "stencil",
            _normalize_stencil(self.stencil, name=f"{self.name}.stencil"),
        )
__post_init__()

Validate the gate stencil and normalize it to a boolean mask.

Source code in tempura/electrostatics/device.py
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
def __post_init__(self) -> None:
    """Validate the gate stencil and normalize it to a boolean mask."""
    super().__post_init__()
    if self.resolution is not None:
        raise ValueError(
            f"{self.name}.resolution is not supported: gates have no "
            "refinement zone of their own and inherit the device-halo "
            "lattice. Remove the resolution argument."
        )
    if self.stencil is None:
        raise ValueError("Gate requires a stencil.")
    object.__setattr__(
        self,
        "stencil",
        _normalize_stencil(self.stencil, name=f"{self.name}.stencil"),
    )

QuantumRegion dataclass

Bases: LayerBase

Quantum material region used as the active electrostatic response layer.

Parameters:

Name Type Description Default
name str

Unique region name. The default downstream extraction helpers look for "quantum_region".

required
thickness float

Region thickness in device units.

required
permittivity float

Relative permittivity assigned to the region.

required
boundary_condition Literal['neumann', 'helmholtz', 'flexible']

Pescado response model for the region. Supported values are "neumann", "helmholtz", and "flexible".

'helmholtz'
resolution ndarray | None

Optional [dx, dy, dz] mesh spacing override. The quantum-region resolution defines the finest master lattice used by automatic meshing.

None
stencil ndarray | None

Optional boolean (ny, nx) mask aligned to Device.grid_shape. When given, the region is patterned to those True cells instead of covering the full XY footprint.

None
mesh_refinement_resolution ndarray | None

Removed legacy spelling. Passing it raises an error so old examples fail loudly.

None

Raises:

Type Description
ValueError

If material parameters are invalid or an unsupported boundary condition is requested.

Source code in tempura/electrostatics/device.py
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
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
@dataclass
class QuantumRegion(LayerBase):
    """Quantum material region used as the active electrostatic response layer.

    Args:
        name: Unique region name. The default downstream extraction helpers
            look for ``"quantum_region"``.
        thickness: Region thickness in device units.
        permittivity: Relative permittivity assigned to the region.
        boundary_condition: Pescado response model for the region. Supported
            values are ``"neumann"``, ``"helmholtz"``, and ``"flexible"``.
        resolution: Optional ``[dx, dy, dz]`` mesh spacing override. The
            quantum-region resolution defines the finest master lattice used by
            automatic meshing.
        stencil: Optional boolean ``(ny, nx)`` mask aligned to
            ``Device.grid_shape``. When given, the region is patterned to those
            ``True`` cells instead of covering the full XY footprint.
        mesh_refinement_resolution: Removed legacy spelling. Passing it raises
            an error so old examples fail loudly.

    Raises:
        ValueError: If material parameters are invalid or an unsupported
            boundary condition is requested.
    """

    permittivity: float
    boundary_condition: Literal["neumann", "helmholtz", "flexible"] = "helmholtz"
    stencil: np.ndarray | None = field(default=None, kw_only=True)
    mesh_refinement_resolution: np.ndarray | None = field(default=None, kw_only=True)

    def __post_init__(self) -> None:
        """Validate the quantum region material parameters and boundary condition."""
        super().__post_init__()
        object.__setattr__(
            self,
            "permittivity",
            _require_positive(self.permittivity, name=f"{self.name}.permittivity"),
        )
        if self.stencil is not None:
            object.__setattr__(
                self,
                "stencil",
                _normalize_stencil(self.stencil, name=f"{self.name}.stencil"),
            )
        valid = {"neumann", "helmholtz", "flexible"}
        if self.boundary_condition not in valid:
            raise ValueError(
                f"boundary_condition must be one of {sorted(valid)}; "
                f"got {self.boundary_condition!r}"
            )
        if self.mesh_refinement_resolution is not None:
            raise ValueError(
                f"{self.name}.mesh_refinement_resolution is no longer supported; "
                "express the finest quantum region z mesh through resolution[2]."
            )
        object.__setattr__(self, "mesh_refinement_resolution", None)
__post_init__()

Validate the quantum region material parameters and boundary condition.

Source code in tempura/electrostatics/device.py
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
def __post_init__(self) -> None:
    """Validate the quantum region material parameters and boundary condition."""
    super().__post_init__()
    object.__setattr__(
        self,
        "permittivity",
        _require_positive(self.permittivity, name=f"{self.name}.permittivity"),
    )
    if self.stencil is not None:
        object.__setattr__(
            self,
            "stencil",
            _normalize_stencil(self.stencil, name=f"{self.name}.stencil"),
        )
    valid = {"neumann", "helmholtz", "flexible"}
    if self.boundary_condition not in valid:
        raise ValueError(
            f"boundary_condition must be one of {sorted(valid)}; "
            f"got {self.boundary_condition!r}"
        )
    if self.mesh_refinement_resolution is not None:
        raise ValueError(
            f"{self.name}.mesh_refinement_resolution is no longer supported; "
            "express the finest quantum region z mesh through resolution[2]."
        )
    object.__setattr__(self, "mesh_refinement_resolution", None)

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

build_problem(device, vacuum_scale=8.0, *, vacuum_resolution_scale=8.0, dirichlet_boundary=True, verbose=False)

Build a Poisson problem for a layered device.

Parameters:

Name Type Description Default
device Device

Finalized device whose electrostatic stack should be meshed.

required
vacuum_scale float

Outer vacuum size multiplier relative to the device span.

8.0
vacuum_resolution_scale float

Coarsening factor applied to the automatic outer-vacuum lattice spacing.

8.0
dirichlet_boundary bool

Whether to add a thin outer shell of zero-volt Dirichlet sites around the simulation box. When False, the outer shell is omitted.

True
verbose bool

Whether to emit progress logging during assembly.

False

Returns:

Type Description
ProblemBuilder

Initialized Pescado :class:ProblemBuilder ready to be finalized

ProblemBuilder

explicitly before calling :func:solve_gate_potentials.

Source code in tempura/electrostatics/pescado_wrapper.py
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
def build_problem(
    device: Device,
    vacuum_scale: float = 8.0,
    *,
    vacuum_resolution_scale: float = 8.0,
    dirichlet_boundary: bool = True,
    verbose: bool = False,
) -> ProblemBuilder:
    """Build a Poisson problem for a layered device.

    Args:
        device: Finalized device whose electrostatic stack should be meshed.
        vacuum_scale: Outer vacuum size multiplier relative to the device span.
        vacuum_resolution_scale: Coarsening factor applied to the automatic
            outer-vacuum lattice spacing.
        dirichlet_boundary: Whether to add a thin outer shell of zero-volt
            Dirichlet sites around the simulation box. When ``False``, the
            outer shell is omitted.
        verbose: Whether to emit progress logging during assembly.

    Returns:
        Initialized Pescado :class:`ProblemBuilder` ready to be finalized
        explicitly before calling :func:`solve_gate_potentials`.
    """
    problem_builder, _region_shapes, _gate_names, _ = build_problem_components(
        device,
        vacuum_scale=vacuum_scale,
        vacuum_resolution_scale=vacuum_resolution_scale,
        dirichlet_boundary=dirichlet_boundary,
        verbose=verbose,
    )
    return problem_builder

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,
    )

solve_gate_potentials(problem, *, charge=None, dtype=DTYPE, blr=BLR, eps_blr=EPS_BLR, rhs_block_size=8, save_quantum_region_potential=None, quantum_region_name='quantum_region', quantum_region_plane_selection='midpoint', verbose=False)

Solve the gate basis problem with one reused factorization.

Parameters:

Name Type Description Default
problem Any

Finalized Pescado problem returned by build_problem(...).finalized().

required
charge ndarray | None

Optional distributed charge columns to include in the RHS.

None
dtype DTypeLike

Numeric dtype used during factorization and solve.

DTYPE
blr bool

Whether to enable MUMPS block low-rank factorization.

BLR
eps_blr float

BLR compression threshold when blr is enabled.

EPS_BLR
rhs_block_size int

Number of gate basis columns to solve per linear solve.

8
save_quantum_region_potential str | Path | None

Optional output directory for static quantum region export.

None
quantum_region_name str

Region name to export when saving a quantum region basis bundle.

'quantum_region'
quantum_region_plane_selection PlaneSelection

Which realized quantum region z plane to export when the region spans multiple z coordinates.

'midpoint'
verbose bool

Whether to emit progress logging during solving.

False

Returns:

Type Description
PotentialMap

Mapping from gate name to solved basis-potential vector.

Source code in tempura/electrostatics/pescado_wrapper.py
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
def solve_gate_potentials(
    problem: Any,
    *,
    charge: np.ndarray | None = None,
    dtype: DTypeLike = DTYPE,
    blr: bool = BLR,
    eps_blr: float = EPS_BLR,
    rhs_block_size: int = 8,
    save_quantum_region_potential: str | Path | None = None,
    quantum_region_name: str = "quantum_region",
    quantum_region_plane_selection: PlaneSelection = "midpoint",
    verbose: bool = False,
) -> PotentialMap:
    """Solve the gate basis problem with one reused factorization.

    Args:
        problem: Finalized Pescado problem returned by
            ``build_problem(...).finalized()``.
        charge: Optional distributed charge columns to include in the RHS.
        dtype: Numeric dtype used during factorization and solve.
        blr: Whether to enable MUMPS block low-rank factorization.
        eps_blr: BLR compression threshold when ``blr`` is enabled.
        rhs_block_size: Number of gate basis columns to solve per linear solve.
        save_quantum_region_potential: Optional output directory for static
            quantum region export.
        quantum_region_name: Region name to export when saving a quantum region
            basis bundle.
        quantum_region_plane_selection: Which realized quantum region z plane to
            export when the region spans multiple z coordinates.
        verbose: Whether to emit progress logging during solving.

    Returns:
        Mapping from gate name to solved basis-potential vector.
    """
    if isinstance(problem, ProblemBuilder):
        raise TypeError(
            "solve_gate_potentials() expects a finalized Pescado problem; call "
            "build_problem(...).finalized() first."
        )
    _region_shapes_from_problem(problem)

    basis_potentials, _plane = solve_gate_potentials_components(
        problem,
        charge=charge,
        dtype=dtype,
        blr=blr,
        eps_blr=eps_blr,
        rhs_block_size=rhs_block_size,
        save_quantum_region_potential=save_quantum_region_potential,
        quantum_region_name=quantum_region_name,
        quantum_region_plane_selection=quantum_region_plane_selection,
        verbose=verbose,
    )
    return basis_potentials