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.
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.1. Header
We begin by importing the necessary Python modules
from pathlib import Path
from qtcad.builder import Builder, Polygon, Mask
The first import brings in Python’s pathlib library, which will be used for
handling file paths.
The second import loads the key classes from the Builder package:
Builder— the main class for constructing the device model and generating the mesh.Mask— defines the two-dimensional layers that theBuildercombines into volumetric regions.Polygon— creates the geometric shapes that are assembled intoMaskobjects.
The basic idea of how three-dimensional device models are built using the
Builder class is to start with a set of
two-dimensional masks (situated in the \(xy\)-plane), each representing
certain features of the device.
These masks are then extruded (along the \(z\)-axis) to form the volumetric
regions of the device, with care being taken to ensure that overlapping regions
are assigned the correct names.
We also set up paths for saving intermediate figures produced during the building process, as well as the final mesh and geometry files.
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]
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.
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.
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)
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).
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.
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
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
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)
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)
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.
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.
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
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"
)