Skip to content

electrostatics.device — the device stack

tempura/electrostatics/device.py defines the user-facing geometry model: the Device container and the three layer types Gate, Dielectric, and QuantumRegion. This is where a stack of dielectrics, metals, and an active region becomes the realized 3D geometry that the mesher and solver consume.

What it does

A Device is an ordered stack assembled bottom-to-top with add_layer(...). Each layer carries a name and a thickness. Dielectrics and quantum regions also carry a permittivity and an optional resolution=[dx, dy, dz]; gates do not take a resolution (they have no refinement zone of their own and inherit the device-halo lattice — passing one raises an error). Gates carry a boolean stencil on the device lattice. A QuantumRegion may optionally carry a stencil too, to pattern the active region into a partial footprint instead of covering the whole device (see Patterned quantum regions).

Layers deposited together at the same height are passed as one list — a coplanar batch. A batch may mix layer types (for example a gate beside a patterned region), with the single rule that the footprints in one batch must be disjoint; overlapping footprints in a batch raise a ValueError. Passing a single layer is shorthand for a one-element batch.

Calling finalize() is the hinge between an editable specification and fixed geometry. Up to that point you are mutating Python objects; after it, Tempura has realized every layer's height fields and 3D shape, and the layer inputs are frozen against further edits.

How finalization works

Finalization walks the deposition batches and evolves a single running top surface height field \(h(x, y)\), initialized to zero on the device grid. Every layer in a batch starts from the same incoming surface \(h\), and the surface then advances to the tallest realized top across the batch:

  • A full-footprint layer (a dielectric, or an unpatterned quantum region) is deposited over the whole footprint. Its bottom follows the current surface and its top follows an overhang base of that surface raised by the layer thickness, so the layer conforms over the topography left by layers below it (see height_fields).
  • A gate deposits metal only where its stencil is True: the bottom sits on the surface and the top is the surface plus the gate thickness, with no overhang.

After the batch, the running surface is raised to \(\max\) of the incoming surface and every realized top in the batch, so the next layer climbs over whatever was just deposited.

Each realized layer stores its bottom/top height arrays \(z_\text{bot}(x,y)\) and \(z_\text{top}(x,y)\) (with NaN outside the footprint), the matching world-space samplers, and a backend shapes.Shape. An unpatterned quantum region becomes a full rectangular slab so its solver plane stays rectangular; every other layer — including a patterned quantum region — becomes a General shape constrained by its height fields and footprint.

Patterned quantum regions

By default a QuantumRegion covers the full device footprint. Passing a stencil patterns it to the True cells, exactly like a gate footprint:

import numpy as np
from tempura import Device, Dielectric, QuantumRegion

active = np.zeros((4, 4), dtype=bool)
active[1:3, 1:3] = True  # a 2x2 patch of active material

device = Device(length=4.0, width=4.0, grid=1.0)
device.add_layer(Dielectric(name="buffer", permittivity=12.9, thickness=2.0))
device.add_layer(
    QuantumRegion(
        name="quantum_region",
        permittivity=12.9,
        thickness=1.0,
        stencil=active,
    )
)
device.finalize()

A patterned region is realized as a footprint-following General shape rather than the full slab. Plane extraction still returns a complete rectangular plane: extract_quantum_region_plane samples the region's own bounding-box slab over its z span, so the patterned cells sit inside an otherwise rectangular grid.

Resolution normalization

Layer resolutions are normalized against the device grid as they are added. The in-plane steps \(d_x, d_y\) must be integer multiples of device.grid. The vertical step \(d_z\) may be coarser (an integer multiple of the grid) or finer (the grid divided by an integer); a requested \(d_z\) that does not evenly divide the grid is rounded up to the nearest divisor, with a warning. This keeps every layer commensurate with the shared lattice that _mesh_plan later refines.

The quantum region sets the finest lattice

The QuantumRegion in-plane resolution must equal device.grid exactly, and it defines the finest master lattice for meshing. Set its dz deliberately (QuantumRegion(resolution=[grid, grid, dz])) when you need a fine vertical mesh inside the active region.

The quantum-region dz is also the single anchor for the vertical lattice, so finalize() requires every layer thickness to be an integer multiple of that dz. This keeps each layer interface on a mesh plane \(z = k\,d_z\); a thickness that does not conform fails immediately, naming the layer, instead of silently producing a mesh that does not match the requested geometry. Likewise, each layer's coarse \(d_x, d_y\) must evenly tile the device length/width, so the realized mesh is not clipped at the high edge.

Boundary condition models

QuantumRegion.boundary_condition selects the linear response model applied to the active region during assembly. The Poisson boundary models Tempura assigns are, for permittivity \(\varepsilon\) and potential \(\phi\):

Model Condition Used for
Dirichlet \(\phi = \phi_0\) grounded shell, metal gates
Neumann \(\hat{\mathbf n}\cdot(\varepsilon\nabla\phi) = g\) dielectrics
Helmholtz \(\hat{\mathbf n}\cdot(\varepsilon\nabla\phi) + \alpha\phi = \beta\) quantum region (default)
Flexible geometry only; response set later quantum region handed to a self-consistent solve

"helmholtz" keeps the active region's linear response inside the linear gate-basis workflow; "flexible" reserves the geometry for a later self-consistent stage; "neumann" treats the region as passive.

API

device

Public device model and layer types used to build electrostatic stacks.

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

LayerBase dataclass

Shared validated inputs for device layers.

The public dataclass fields are the user-provided layer specification. Realized solver geometry is attached later by :meth:Device.finalize and exposed through read-only properties such as :attr:z_bot_arr and :attr:shape.

Attributes:

Name Type Description
name str

Unique layer name inside one :class:Device. The name is also used as the solver region name after meshing.

thickness float

Layer thickness in the same internal units as the parent device.

resolution ndarray | None

Optional [dx, dy, dz] mesh spacing override. If omitted, the device chooses a spacing from device.grid and the layer thickness.

Source code in tempura/electrostatics/device.py
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
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
@dataclass
class LayerBase:
    """Shared validated inputs for device layers.

    The public dataclass fields are the user-provided layer specification.
    Realized solver geometry is attached later by :meth:`Device.finalize` and
    exposed through read-only properties such as :attr:`z_bot_arr` and
    :attr:`shape`.

    Attributes:
        name: Unique layer name inside one :class:`Device`. The name is also
            used as the solver region name after meshing.
        thickness: Layer thickness in the same internal units as the parent
            device.
        resolution: Optional ``[dx, dy, dz]`` mesh spacing override. If omitted,
            the device chooses a spacing from ``device.grid`` and the layer
            thickness.
    """

    name: str
    thickness: float
    resolution: np.ndarray | None = field(default=None, kw_only=True)

    _frozen: bool = field(init=False, default=False, repr=False)
    _finalized_geometry: FinalizedLayerGeometry | None = field(
        init=False,
        default=None,
        repr=False,
    )

    def __setattr__(self, name: str, value: object) -> None:
        """Freeze public fields once finalized while allowing private caches."""
        if getattr(self, "_frozen", False) and not name.startswith("_"):
            raise FrozenInstanceError(f"cannot assign to field {name!r}")
        object.__setattr__(self, name, value)

    def __post_init__(self) -> None:
        """Normalize the public layer specification into validated NumPy inputs."""
        object.__setattr__(self, "name", str(self.name))
        object.__setattr__(
            self,
            "thickness",
            _require_positive(self.thickness, name=f"{self.name}.thickness"),
        )
        if self.resolution is None:
            object.__setattr__(self, "resolution", None)
        else:
            object.__setattr__(
                self,
                "resolution",
                _normalize_resolution(self.resolution, name=f"{self.name}.resolution"),
            )

    @property
    def z_bot_arr(self) -> np.ndarray | None:
        """Return the finalized bottom height field, if available."""
        geometry = self._finalized_geometry
        return None if geometry is None else cast(np.ndarray, geometry["z_bot_arr"])

    @property
    def z_top_arr(self) -> np.ndarray | None:
        """Return the finalized top height field, if available."""
        geometry = self._finalized_geometry
        return None if geometry is None else cast(np.ndarray, geometry["z_top_arr"])

    @property
    def z_bot_fn(self) -> Callable[[float, float], float] | None:
        """Return the finalized bottom height sampler, if available."""
        geometry = self._finalized_geometry
        return (
            None
            if geometry is None
            else cast(Callable[[float, float], float], geometry["z_bot_fn"])
        )

    @property
    def z_top_fn(self) -> Callable[[float, float], float] | None:
        """Return the finalized top height sampler, if available."""
        geometry = self._finalized_geometry
        return (
            None
            if geometry is None
            else cast(Callable[[float, float], float], geometry["z_top_fn"])
        )

    @property
    def shape(self) -> shapes.Shape | None:
        """Return the finalized backend shape, if available."""
        geometry = self._finalized_geometry
        return None if geometry is None else cast(shapes.Shape, geometry["shape"])

    def _set_finalized_geometry(self, geometry: FinalizedLayerGeometry) -> None:
        """Attach finalized solver geometry to this layer instance."""
        object.__setattr__(self, "_finalized_geometry", geometry)

    def _freeze(self) -> None:
        """Freeze public and finalized array buffers after successful realization."""
        if isinstance(getattr(self, "stencil", None), np.ndarray):
            self.stencil.setflags(write=False)
        if isinstance(self.resolution, np.ndarray):
            self.resolution.setflags(write=False)
        mesh_refinement_resolution = getattr(self, "mesh_refinement_resolution", None)
        if isinstance(mesh_refinement_resolution, np.ndarray):
            mesh_refinement_resolution.setflags(write=False)
        geometry = self._finalized_geometry
        if geometry is not None:
            cast(np.ndarray, geometry["z_bot_arr"]).setflags(write=False)
            cast(np.ndarray, geometry["z_top_arr"]).setflags(write=False)
        object.__setattr__(self, "_frozen", True)

    def _require_finalized_geometry(self) -> FinalizedLayerGeometry:
        """Return finalized geometry or raise if finalization has not happened."""
        geometry = self._finalized_geometry
        if geometry is None:
            raise ValueError("Layer geometry is not available before device.finalize().")
        return geometry

    def _sample_height_bounds(
        self,
        x: np.ndarray,
        y: np.ndarray,
    ) -> tuple[np.ndarray, np.ndarray]:
        """Return vectorized bottom/top height samples for world coordinates."""
        geometry = self._require_finalized_geometry()
        device_length = float(cast(float, geometry["device_length"]))
        device_width = float(cast(float, geometry["device_width"]))
        return (
            sample_height_array(
                cast(np.ndarray, geometry["z_bot_arr"]),
                device_length,
                device_width,
                x,
                y,
            ),
            sample_height_array(
                cast(np.ndarray, geometry["z_top_arr"]),
                device_length,
                device_width,
                x,
                y,
            ),
        )
shape property

Return the finalized backend shape, if available.

z_bot_arr property

Return the finalized bottom height field, if available.

z_bot_fn property

Return the finalized bottom height sampler, if available.

z_top_arr property

Return the finalized top height field, if available.

z_top_fn property

Return the finalized top height sampler, if available.

__post_init__()

Normalize the public layer specification into validated NumPy inputs.

Source code in tempura/electrostatics/device.py
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
def __post_init__(self) -> None:
    """Normalize the public layer specification into validated NumPy inputs."""
    object.__setattr__(self, "name", str(self.name))
    object.__setattr__(
        self,
        "thickness",
        _require_positive(self.thickness, name=f"{self.name}.thickness"),
    )
    if self.resolution is None:
        object.__setattr__(self, "resolution", None)
    else:
        object.__setattr__(
            self,
            "resolution",
            _normalize_resolution(self.resolution, name=f"{self.name}.resolution"),
        )
__setattr__(name, value)

Freeze public fields once finalized while allowing private caches.

Source code in tempura/electrostatics/device.py
346
347
348
349
350
def __setattr__(self, name: str, value: object) -> None:
    """Freeze public fields once finalized while allowing private caches."""
    if getattr(self, "_frozen", False) and not name.startswith("_"):
        raise FrozenInstanceError(f"cannot assign to field {name!r}")
    object.__setattr__(self, name, value)

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)