1. Mesh generation using QTCAD Builder

1.1. Requirements

1.1.1. Software components

  • QTCAD

  • Gmsh

1.1.2. Python script

  • qtcad/examples/practical_application/FDSOI/1-fdsoi_builder.py

1.1.3. References

1.2. Briefing

This tutorial demonstrates how to use the QTCAD Builder package to construct a realistic three-dimensional fully-depleted silicon on insulator (FD-SOI) device and generate a corresponding 3D finite-element mesh. This provides an alternative to the standard Gmsh API (used, for example, in A double quantum dot device in a fully-depleted silicon-on-insulator transistor) and is often a more straightforward workflow for creating meshes for QTCAD.

The device considered here is the FD-SOI structure shown in Fig. 1.2.2, taken from A double quantum dot device in a fully-depleted silicon-on-insulator transistor.

Double dot FDSOI structure

Fig. 1.2.2 Fully-depleted silicon-on-insulator (FD-SOI) structure hosting a gated double quantum dot. (a) Three-dimensional view of the structure, with colored surfaces indicating named boundaries. The gray color indicates regions where the material is silicon dioxide, and for which simulation domain boundaries obey default (natural) boundary conditions. (b) Slice across the channel. (c) Slice along the channel. Regions colored with the pale shade of green are intrinsic silicon, while the darker green regions are strongly n-doped silicon.

The device consists of a rectangular silicon channel (pale green in Fig. 1.2.2) with source and drain contacts on either side, positioned above a buried oxide (BOX) layer (gray). Metallic gates on the top surface serve as plunger gates (for electron confinement) and barrier gates (to control tunneling between different regions) and are separated from the channel by a thin gate oxide layer. In this tutorial, one plunger gate defines a quantum dot in one half of the channel, while the second plunger gate defines a second dot in the other half. The first dot functions as a single-electron transistor (SET), while the second dot can be thought of as a qubit. In what follows, we will refer to these two quantum dots as “SET” and “qubit”, respectively.

Further details on the device geometry and dimensions are available in A double quantum dot device in a fully-depleted silicon-on-insulator transistor.

1.3. Setup

1.3.2. Length scales and constants

Next, we define some length scales that will be used in our model device. These parameters can be altered to produce devices with the same topology but different physical dimensions.

# Length scales ---------------------------------------------------------------
char_len            = 4         # characteristic mesh length

domain_w            = 60        # width of the simulation domain
domain_l            = 130       # length of the simulation domain
channel_w           = 40        # width of the silicon channel
plunger_w           = 15        # width of the plunger gates
barrier_w           = 10        # width of the barrier gates
source_drain_w      = 20        # width of the source and drain regions
pitch               = 5         # distance between the gates
# Width of the quantum-dot regions
QD_w                = plunger_w + barrier_w + 2*pitch
# Thicknesses
box_thick           = 10        # Buried oxide thickness
channel_thick       = 10        # Channel thickness
EOT_thick           = 2         # Equivalent oxide thickness (gate oxide)

The first parameter, char_len, sets the characteristic length of the mesh across the entire device. Adjusting it refines or coarsens the mesh globally. It effectively controls the approximate spacing between adjacent mesh nodes. The remaining variables specify the dimensions of the various device components, and their roles will become clear in the sections that follow.

1.4. Creating the masks

With the setup complete, we now create the two-dimensional masks that will serve as the basis for the three-dimensional device model. These masks are built using the Polygon and Mask classes.

We begin by defining the channel_mask, which will later be extruded to form the silicon channel.

# Masks -----------------------------------------------------------------------

# Silicon channel
channel         = Polygon.box(channel_w, domain_l, name="channel").centered()
channel_mask    = Mask("channel")
channel_mask.add_shape(channel)

We begin by creating a rectangle using Polygon.box, with width channel_w and length domain_l. The shape is named "channel" (optional but helpful for later identification) and centered at the origin using centered. Without calling this method, the rectangle would be positioned with its lower-left corner at the origin. This rectangle is then added to a new Mask called channel_mask via add_shape.

We follow the same approach to build the remaining Mask objects required for the device. The next one, oxide_mask, defines the oxide layer surrounding the silicon channel and the gate contacts.

# oxides - Full domain
oxide           = Polygon.box(domain_w, domain_l, name="oxide").centered()
oxide_mask      = Mask("oxide")
oxide_mask.add_shape(oxide)

We continue by creating the masks that will later be used to create the source and drain regions of the device and the top gates.

# Source and drain
source          = Polygon.box(
                    channel_w, source_drain_w, name="source"
                    ).centered(
                    ).translated(
                    0, -(domain_l - source_drain_w) / 2
                    )
drain           = Polygon.box(
                    channel_w, source_drain_w, name="drain"
                    ).centered(
                    ).translated(
                    0, (domain_l - source_drain_w) / 2
                    )

sd_mask         = Mask("source_drain")
sd_mask.add_shapes([source, drain])

# Gates
B1              = Polygon.box(
                    domain_w, barrier_w, name="B1"
                    ).centered(
                    ).translated(
                    0, -(barrier_w + 2 * pitch + plunger_w)
                    )
P1              = Polygon.box(
                    domain_w, plunger_w, name="P1"
                    ).centered(
                    ).translated(
                    0, -(barrier_w / 2 + pitch + plunger_w / 2)
                    )
B2              = Polygon.box(
                    domain_w, barrier_w, name="B2"
                    ).centered(
                    )
P2              = Polygon.box(
                    domain_w, plunger_w, name="P2"
                    ).centered(
                    ).translated(
                    0, (barrier_w / 2 + pitch + plunger_w / 2)
                    )
B3              = Polygon.box(
                    domain_w, barrier_w, name="B3"
                    ).centered(
                    ).translated(
                    0, barrier_w + 2 * pitch + plunger_w
                    )

gate_mask      = Mask("gates")
gate_mask.add_shapes([B1, P1, B2, P2, B3])

Finally, we create two additional masks which will be used to define the quantum dot (qubit) region and the single-electron transistor (SET) region, respectively.

# Quantum dot regions
# (SET)
QD1             = Polygon.box(
                    channel_w+pitch, QD_w, name="QD1"
                    ).centered(
                    ).translated(
                    0, -(barrier_w / 2 + pitch + plunger_w / 2)
                    )
# (Qubit)
QD2             = Polygon.box(
                    channel_w+pitch, QD_w, name="QD2"
                    ).centered(
                    ).translated(
                    0, barrier_w/2 + pitch + plunger_w / 2
                    )

dot_mask        = Mask("dots")
dot_mask.add_shapes([QD1, QD2])

1.5. Building the device

1.5.1. Setting up the Builder

With the masks defined, we can now set up the Builder to construct the three-dimensional device model.

# Setup builder ---------------------------------------------------------------
builder = Builder()
# Set some global parameters
builder.set_mesh_size(char_len).number_hull_surfaces(True)

# Add the masks to the builder
builder.add_mask(
    channel_mask
    ).add_mask(
    oxide_mask
    ).add_mask(
    sd_mask
    ).add_mask(
    gate_mask
    ).add_mask(
    dot_mask
    )

We begin by creating an instance of the Builder class. Next, we configure global parameters using set_mesh_size and number_hull_surfaces:

  • set_mesh_size — sets the characteristic length of the mesh across the device (this can also be specified later when saving the mesh).

  • number_hull_surfaces — numbers the side surfaces (all surfaces other than the top and bottom) of newly created volumes (the hull being the set of outer surfaces of the volume). Setting this parameter is useful when specific side surfaces of a volume need to be identified, as demonstrated later.

Finally, we add the previously defined masks to the builder using add_mask.

Note

An instance of the Builder class, e.g. builder, contains methods that return the builder variable itself. Therefore method calls can be chained together as shown above.

The Builder class is also equipped with methods to visualize the masks and the generated geometry at different stages of the building process. Here, we will use the view_shapes method to visualize all the masks created above and save the resulting figure as an .svg file.

# Visualize the masks
builder.use_all_masks(
    ).view_shapes(
    save=masks_file,
    font_size=20
    )

We start by constructing a temporary mask which is a combination of all the masks added to the builder using the use_all_masks method. This mask is not added to the builder but is only used for visualization purposes. We then visualize the shapes in this mask using the view_shapes method. We specify that we want to save the figure to the path specified by masks_file and set the font size for the labels to 20. The resulting figure is shown below.

Layout used to construct the FD-SOI device.

Fig. 1.5.6 The layout used to construct the FD-SOI device showing the different masks created using the Mask class. The masks include the silicon channel, oxides (whose mask also makes up the entire simulation domain), the source and drain, the gates, and the qubit and SET regions. The names of the different shapes are overlapping in this figure because the different masks are stacked on top of each other at the same position in the xy plane. However, the silicon channel mask and the oxide mask at the center should be visible. Below, we will show how these masks are used to create the three-dimensional model of the device.

Note

The view_shapes and view methods automatically launch an instance of the Gmsh GUI environment that enables users to interactively explore the structures/geometries being visualized.

1.5.2. Building the BOX layer

The first volume we create is the buried BOX layer using the use_mask and extrude methods.

# Build the BOX ---------------------------------------------------------------
builder.use_mask("oxide")
builder.view_shapes(save=oxide_mask_file, font_size=20)

We first tell the builder to use the Mask named "oxide" using the use_mask method. We start by visualizing this mask using the view_shapes method.

Layout of the silicon ``"oxide"`` mask.

Fig. 1.5.7 Layout of the silicon "oxide" mask used to create the simulation domain and the BOX, shallow trench isolation (STI), and gate-oxide volumes. For more details, see A double quantum dot device in a fully-depleted silicon-on-insulator transistor.

It is a rectangle centered at the origin with width domain_w and length domain_l, as defined above.

We then position this two-dimensional mask at z = -box_thick - channel_thick using the set_z method and extrude it along the growth direction (z) by a length box_thick using the extrude method. The resulting structure can be visualized using the view method and is reproduced as Fig. 1.5.8, below. We specify that we want to see the surface and volume labels to help with identification of the different parts of the model.

builder.set_z(-box_thick - channel_thick).extrude(box_thick)    # BOX volume
builder.view(surface_labels=True, volume_labels=True, angles=v_angles, save=BOX_file)
Model of the BOX generated by extruding the "oxide" mask.

Fig. 1.5.8 BOX volume with surface and volume labels generated by extruding the "oxide" mask.

In the figure above, we can see that the oxide volume has been created correctly. The side surfaces of the oxide volume have been automatically named "oxide_side[0]", "oxide_side[1]", "oxide_side[2]", and "oxide_side[3]", while the top and bottom surfaces have been named "oxide_top" and "oxide_bottom", respectively. The side surfaces are numbered because we have set number_hull_surfaces to True. The volume itself has inherited its name from the mask and has been named "oxide".

Note

By default, the names of volumes in the Gmsh GUI opened with Builder will be printed in yellow text, while surface names will be printed in orange text.

While the automatic naming of surfaces can be helpful, using descriptive names often makes device modeling clearer. Here, we rename the bottom surface, corresponding to the back-gate contact, to "back_gate_bnd" using rename_group.

To avoid clutter, we remove unnecessary surfaces with dissolve_physical_group. The argument of this method is an identifier of groups, in this case, a function that identifies which groups to delete. Here, we dissolve any two-dimensional group whose name contains the substring "oxide".

# Rename the bottom oxide surface to back gate boundary
builder.rename_group("oxide_bottom", "back_gate_bnd")
# Remove unnecessary surfaces
builder.dissolve_physical_group(lambda g: ("oxide" in g.name and g.dim == 2))
builder.view(surface_labels=True, volume_labels=True, angles=v_angles, save=BOX_rename_file)

Using the view method again, we can verify that the naming has been updated (see Fig. 1.5.9).

Model of the BOX with the surfaces appropriatedly renamed.

Fig. 1.5.9 BOX volume with surface and volume labels after renaming the back gate boundary surface and dissolving unnecessary surfaces.

1.5.3. Building the silicon channel

Next, we build the silicon channel volume which sits on top of the BOX layer.

# Build the channel -----------------------------------------------------------
builder.use_mask("channel")
builder.set_z(-channel_thick).extrude(channel_thick)            # Channel volume
# Remove unnecessary surfaces
builder.dissolve_physical_group(lambda g: ("channel" in g.name and g.dim == 2))
builder.view(surface_labels=True, volume_labels=True, angles=v_angles, save=channel_file)

We proceed in the same way as before by telling the builder to use the "channel" mask using the use_mask method. We then position the mask at z = -channel_thick using the set_z method and extrude it by a length channel_thick using the extrude method. After the volume is created, we again dissolve any unnecessary surfaces using the dissolve_physical_group method. The resulting structure can be visualized below.

Volume generated by extruding the "channel" mask.

Fig. 1.5.10 Silicon channel sitting on top of the BOX layer after dissolving the unnecessary surfaces.

1.5.4. Building the STI regions

Before building the oxides and gates, we switch the builder to fill mode using the fill_mode method. In this mode, volumes that overlap with existing ones are built so that the overlapping regions inherit the physical groups/names of the volumes created first.

# Build the STI regions -------------------------------------------------------
builder.fill_mode()

Now that the mode has been set, we proceed to create the oxide layer on either side of the channel, above the BOX layer. We create associated volumes following the same procedure as before using the set_z_from_group and extrude methods. In this case, we use the set_z_from_group method to position the oxide mask at the bottom of the channel volume instead of specifying it using the set_z method.

builder.use_mask("oxide").set_z_from_group("channel", bottom=True)
builder.extrude(channel_thick)                                  # STI volume
builder.view(surface_labels=True, volume_labels=True, angles=v_angles, save=STI_file)

The resulting three-dimensional model is shown below

Model containing STI regions on either side of the silicon channel.

Fig. 1.5.11 Volume generated by extruding the "oxide" mask to create the shallow trench isolation (STI) regions. Because the builder was set to fill_mode, the name "channel" takes precedence over the names of the entities generated from the "oxide" mask because the "channel" volume was created first.

Note the naming of the surfaces and volumes in the figure above. Whenever a surface or volume is created from the intersection of a newly created volume with the existing "channel" volume, the names inherited from the "channel" mask take precedence over those from the "oxide" mask. This occurs because the channel volume was created first and the builder is operating in fill_mode.

After the volume is created, we again dissolve any unnecessary surfaces.

# Remove unnecessary surfaces
builder.dissolve_physical_group(lambda g: ("oxide" in g.name and g.dim == 2))
builder.dissolve_physical_group(lambda g: ("channel" in g.name and g.dim == 2))
builder.view(surface_labels=True, volume_labels=True, angles=v_angles, save=STI_rename_file)

The resulting model is shown below

Model containing STI regions on either side of the silicon channel after renaming some surfaces and dissolving unnecessary surfaces.

Fig. 1.5.12 Volume generated by extruding the "oxide" mask to create the STI regions after dissolving unnecessary surfaces.

1.5.5. Building the source and drain regions

The next step is to build the source and drain volumes and surfaces. We start by switching the builder to displace mode using the displace_mode method. Contrary to fill_mode, displace_mode creates volumes that displace previously created volumes. In other words, the new volume will “carve out” a region from the existing volumes leading to any overlapping regions being assigned the label of the newly created volume.

# Build source and drain regions ----------------------------------------------
builder.displace_mode()

Now, we can create the lead regions using the same procedure as before. These regions sit at either end of the channel and extend through its full thickness.

# Build the volumes
builder.set_z_from_group("channel", bottom=True)
builder.use_mask("source_drain")
builder.extrude(channel_thick)                                  # Lead volumes
builder.view(surface_labels=True, volume_labels=True, angles=v_angles, save=sd_file)
Model containing source and drain regions.

Fig. 1.5.13 Volume generated by extruding the "source_drain" mask to create the source and drain regions.

Again we relabel the source and drain boundaries to more descriptive names using the rename_group method and dissolve any unnecessary surfaces using the dissolve_physical_group

# Rename source and drain boundaries
builder.rename_group("source_side[3]", "source_bnd")
builder.rename_group("drain_side[1]", "drain_bnd")
# Remove unnecessary surfaces
builder.dissolve_physical_group(lambda g: ("source" in g.name and g.dim == 2 and "bnd" not in g.name))
builder.dissolve_physical_group(lambda g: ("drain" in g.name and g.dim == 2 and "bnd" not in g.name))

builder.view(surface_labels=True, volume_labels=True, angles=v_angles, save=sd_rename_file)
Model containing source and drain regions.

Fig. 1.5.14 Volume generated by extruding the "source_drain" mask to create the source and drain regions, after renaming the source and drain boundaries and dissolving unnecessary surfaces.

Note

Labeling the source and drain boundaries as "source_bnd" and "drain_bnd" respectively was only possible because we had enabled the number_hull_surfaces option when setting up the builder. Without this option enabled, the side surfaces of the generated volumes would not have been numbered and we would not have been able to identify which surfaces to rename.

Now that we have finished renaming the surfaces, we can disable the number_hull_surfaces to avoid unnecessary surface labels when visualizing the model.

builder.number_hull_surfaces(False)

1.5.6. Building the Gates

Building the gates requires two steps. The first is to generate the gate-oxide layer below the gates. This is done in a similar manner as before using the use_mask and extrude methods

# Build gate-oxide layer ------------------------------------------------------
builder.use_mask("oxide").extrude(EOT_thick)                    # gate-oxide volume
# Remove unnecessary surfaces
builder.dissolve_physical_group(lambda g: ("oxide" in g.name and g.dim == 2))

Notice that we did not need to set the position of the oxide mask before extruding it. This is because the mask is already positioned at the top of the channel volume after building the source and drain regions above. By default, the position of the mask being used is set to the top of the last volume created.

Once the oxide layer is created, we can proceed to deposit the gates.

# Deposit gates ---------------------------------------------------------------
builder.use_mask("gates")
builder.add_surface()

builder.view(surface_labels=True, volume_labels=True, angles=v_angles, save=gates_file)

Because the gates are modelled as surfaces, we need only select the "gates" mask using the use_mask method and then call the add_surface method to create the surfaces at the position of the mask.

1.5.7. Building the dot regions

The final step is to build the qubit and SET regions. These regions are subregions of the device that will be used in later scripts to specify where we will solve the Schrödinger equation. These regions will be situated below the plunger gates and extend into the STI region and the gate oxide region.

We start by switching the builder to overlay mode using the overlay_mode method. This is yet a third alternative mode for building volumes (the other two being fill_mode and displace_mode). In this mode, new volumes overlapping with previously created volumes are given a distinct label. In particular the label for the overlapping region is "fist_name.second_name", where "first_name" is the name of the entity created first and "second_name" is the name of the newly created entity.

# Build dot regions -----------------------------------------------------------
builder.overlay_mode()
builder.use_mask("dots").set_z_from_group("channel", bottom=True)
builder.extrude(channel_thick+EOT_thick)

builder.view(surface_labels=True, volume_labels=True, angles=v_angles, save=dot_file)

In the figure below (Fig. 1.5.15), we can see the resulting model. Because the newly created dot regions overlap with the channel, STI, and gate-oxide volumes, we can see that the overlapping regions have been assigned new volume labels according to the convention described above.

Model containing dot regions.

Fig. 1.5.15 Volume generated by extruding the "dots" mask to create the dot regions. Because the builder was set to overlay_mode, the overlapping regions between the dot regions and the channel, STI, and gate-oxide volumes have been assigned new volume labels: "first_label.QDi" where "first_label" is the label of the volume created first and "i" is either 1 or 2 depending on which dot region is being referred to.

We now clean up the model by renaming gates using the merge_groups method and dissolving unnecessary surfaces.

# Merge gate groups
builder.merge_groups(lambda g: ("P1" in g.name and g.dim == 2), "plunger_gate_1_bnd")
builder.merge_groups(lambda g: ("P2" in g.name and g.dim == 2), "plunger_gate_2_bnd")
builder.merge_groups(lambda g: ("B1" in g.name and g.dim == 2), "barrier_gate_1_bnd")
builder.merge_groups(lambda g: ("B2" in g.name and g.dim == 2), "barrier_gate_2_bnd")
builder.merge_groups(lambda g: ("B3" in g.name and g.dim == 2), "barrier_gate_3_bnd")
# Remove unnecessary surfaces
builder.dissolve_physical_group(lambda g: ("QD" in g.name and g.dim == 2))

builder.view(surface_labels=True, volume_labels=True, angles=v_angles, save=dot_rename_file)

We also visualize the updated model, which is in fact the fully constructed model of the FD-SOI device.

Model containing dot regions.

Fig. 1.5.16 Final model of the FD-SOI device after renaming the gate boundaries and dissolving unnecessary surfaces.

1.6. Generating the mesh

The final step is to generate the mesh and save it to file. This is done using the mesh and write methods. We also save the geometry in an .xao file for use in adaptive meshing later on.

# Save the mesh ---------------------------------------------------------------
builder.mesh()
builder.view(angles=v_angles, save=view_mesh_file)
builder.write(
    script_dir / "meshes" / "dqdfdsoi.msh"
    ).write(
    script_dir / "meshes" / "dqdfdsoi.xao"
    )

The resulting mesh is shown below

Mesh generated over the FD-SOI device model.

Fig. 1.6.2 Mesh generated over the FD-SOI device model.

1.7. Full code

__copyright__ = "Copyright 2022-2025, Nanoacademic Technologies Inc."

from pathlib import Path
from qtcad.builder import Builder, Polygon, Mask

script_dir          = Path(__file__).parent.resolve()
pa_dir              = script_dir.parent.resolve() / "figs"

masks_file          = pa_dir / "fdsoi_masks.svg"
oxide_mask_file     = pa_dir / "fdsoi_oxide_mask.svg"
BOX_file            = pa_dir / "fdsoi_BOX.svg"
BOX_rename_file     = pa_dir / "fdsoi_BOX_renamed.svg"
channel_file        = pa_dir / "fdsoi_channel.svg"
STI_file            = pa_dir / "fdsoi_STI.svg"
STI_rename_file     = pa_dir / "fdsoi_STI_renamed.svg"
sd_file             = pa_dir / "fdsoi_sd.svg"
sd_rename_file      = pa_dir / "fdsoi_sd_renamed.svg"
gates_file          = pa_dir / "fdsoi_gates.svg"
dot_file            = pa_dir / "fdsoi_dot.svg"
dot_rename_file     = pa_dir / "fdsoi_dot_renamed.svg"
view_mesh_file      = pa_dir / "fdsoi_mesh.png"

# view angles
v_angles = [-65, 0, -22.5]

# Length scales ---------------------------------------------------------------
char_len            = 4         # characteristic mesh length

domain_w            = 60        # width of the simulation domain
domain_l            = 130       # length of the simulation domain
channel_w           = 40        # width of the silicon channel
plunger_w           = 15        # width of the plunger gates
barrier_w           = 10        # width of the barrier gates
source_drain_w      = 20        # width of the source and drain regions
pitch               = 5         # distance between the gates
# Width of the quantum-dot regions
QD_w                = plunger_w + barrier_w + 2*pitch
# Thicknesses
box_thick           = 10        # Buried oxide thickness
channel_thick       = 10        # Channel thickness
EOT_thick           = 2         # Equivalent oxide thickness (gate oxide)

# Masks -----------------------------------------------------------------------

# Silicon channel
channel         = Polygon.box(channel_w, domain_l, name="channel").centered()
channel_mask    = Mask("channel")
channel_mask.add_shape(channel)


# oxides - Full domain
oxide           = Polygon.box(domain_w, domain_l, name="oxide").centered()
oxide_mask      = Mask("oxide")
oxide_mask.add_shape(oxide)

# Source and drain
source          = Polygon.box(
                    channel_w, source_drain_w, name="source"
                    ).centered(
                    ).translated(
                    0, -(domain_l - source_drain_w) / 2
                    )
drain           = Polygon.box(
                    channel_w, source_drain_w, name="drain"
                    ).centered(
                    ).translated(
                    0, (domain_l - source_drain_w) / 2
                    )

sd_mask         = Mask("source_drain")
sd_mask.add_shapes([source, drain])

# Gates
B1              = Polygon.box(
                    domain_w, barrier_w, name="B1"
                    ).centered(
                    ).translated(
                    0, -(barrier_w + 2 * pitch + plunger_w)
                    )
P1              = Polygon.box(
                    domain_w, plunger_w, name="P1"
                    ).centered(
                    ).translated(
                    0, -(barrier_w / 2 + pitch + plunger_w / 2)
                    )
B2              = Polygon.box(
                    domain_w, barrier_w, name="B2"
                    ).centered(
                    )
P2              = Polygon.box(
                    domain_w, plunger_w, name="P2"
                    ).centered(
                    ).translated(
                    0, (barrier_w / 2 + pitch + plunger_w / 2)
                    )
B3              = Polygon.box(
                    domain_w, barrier_w, name="B3"
                    ).centered(
                    ).translated(
                    0, barrier_w + 2 * pitch + plunger_w
                    )

gate_mask      = Mask("gates")
gate_mask.add_shapes([B1, P1, B2, P2, B3])

# Quantum dot regions
# (SET)
QD1             = Polygon.box(
                    channel_w+pitch, QD_w, name="QD1"
                    ).centered(
                    ).translated(
                    0, -(barrier_w / 2 + pitch + plunger_w / 2)
                    )
# (Qubit)
QD2             = Polygon.box(
                    channel_w+pitch, QD_w, name="QD2"
                    ).centered(
                    ).translated(
                    0, barrier_w/2 + pitch + plunger_w / 2
                    )

dot_mask        = Mask("dots")
dot_mask.add_shapes([QD1, QD2])

# Setup builder ---------------------------------------------------------------
builder = Builder()
# Set some global parameters
builder.set_mesh_size(char_len).number_hull_surfaces(True)

# Add the masks to the builder
builder.add_mask(
    channel_mask
    ).add_mask(
    oxide_mask
    ).add_mask(
    sd_mask
    ).add_mask(
    gate_mask
    ).add_mask(
    dot_mask
    )

# Visualize the masks
builder.use_all_masks(
    ).view_shapes(
    save=masks_file,
    font_size=20
    )

# Build the BOX ---------------------------------------------------------------
builder.use_mask("oxide")
builder.view_shapes(save=oxide_mask_file, font_size=20)

builder.set_z(-box_thick - channel_thick).extrude(box_thick)    # BOX volume
builder.view(surface_labels=True, volume_labels=True, angles=v_angles, save=BOX_file)

# Rename the bottom oxide surface to back gate boundary
builder.rename_group("oxide_bottom", "back_gate_bnd")  
# Remove unnecessary surfaces   
builder.dissolve_physical_group(lambda g: ("oxide" in g.name and g.dim == 2))
builder.view(surface_labels=True, volume_labels=True, angles=v_angles, save=BOX_rename_file)

# Build the channel -----------------------------------------------------------
builder.use_mask("channel")
builder.set_z(-channel_thick).extrude(channel_thick)            # Channel volume
# Remove unnecessary surfaces 
builder.dissolve_physical_group(lambda g: ("channel" in g.name and g.dim == 2))
builder.view(surface_labels=True, volume_labels=True, angles=v_angles, save=channel_file)

# Build the STI regions -------------------------------------------------------
builder.fill_mode()

builder.use_mask("oxide").set_z_from_group("channel", bottom=True)
builder.extrude(channel_thick)                                  # STI volume
builder.view(surface_labels=True, volume_labels=True, angles=v_angles, save=STI_file)

# Remove unnecessary surfaces 
builder.dissolve_physical_group(lambda g: ("oxide" in g.name and g.dim == 2))
builder.dissolve_physical_group(lambda g: ("channel" in g.name and g.dim == 2))
builder.view(surface_labels=True, volume_labels=True, angles=v_angles, save=STI_rename_file)

# Build source and drain regions ----------------------------------------------
builder.displace_mode()

# Build the volumes
builder.set_z_from_group("channel", bottom=True)
builder.use_mask("source_drain")
builder.extrude(channel_thick)                                  # Lead volumes   
builder.view(surface_labels=True, volume_labels=True, angles=v_angles, save=sd_file)

# Rename source and drain boundaries
builder.rename_group("source_side[3]", "source_bnd")     
builder.rename_group("drain_side[1]", "drain_bnd") 
# Remove unnecessary surfaces 
builder.dissolve_physical_group(lambda g: ("source" in g.name and g.dim == 2 and "bnd" not in g.name))
builder.dissolve_physical_group(lambda g: ("drain" in g.name and g.dim == 2 and "bnd" not in g.name))

builder.view(surface_labels=True, volume_labels=True, angles=v_angles, save=sd_rename_file)

builder.number_hull_surfaces(False)

# Build gate-oxide layer ------------------------------------------------------
builder.use_mask("oxide").extrude(EOT_thick)                    # gate-oxide volume
# Remove unnecessary surfaces 
builder.dissolve_physical_group(lambda g: ("oxide" in g.name and g.dim == 2))

# Deposit gates ---------------------------------------------------------------
builder.use_mask("gates")
builder.add_surface()

builder.view(surface_labels=True, volume_labels=True, angles=v_angles, save=gates_file)

# Build dot regions -----------------------------------------------------------
builder.overlay_mode()
builder.use_mask("dots").set_z_from_group("channel", bottom=True)
builder.extrude(channel_thick+EOT_thick) 

builder.view(surface_labels=True, volume_labels=True, angles=v_angles, save=dot_file)

# Merge gate groups
builder.merge_groups(lambda g: ("P1" in g.name and g.dim == 2), "plunger_gate_1_bnd")
builder.merge_groups(lambda g: ("P2" in g.name and g.dim == 2), "plunger_gate_2_bnd")
builder.merge_groups(lambda g: ("B1" in g.name and g.dim == 2), "barrier_gate_1_bnd")
builder.merge_groups(lambda g: ("B2" in g.name and g.dim == 2), "barrier_gate_2_bnd")
builder.merge_groups(lambda g: ("B3" in g.name and g.dim == 2), "barrier_gate_3_bnd")
# Remove unnecessary surfaces
builder.dissolve_physical_group(lambda g: ("QD" in g.name and g.dim == 2))

builder.view(surface_labels=True, volume_labels=True, angles=v_angles, save=dot_rename_file)

# Save the mesh ---------------------------------------------------------------
builder.mesh()
builder.view(angles=v_angles, save=view_mesh_file)
builder.write(
    script_dir / "meshes" / "dqdfdsoi.msh"
    ).write(
    script_dir / "meshes" / "dqdfdsoi.xao"
    )