Note
Go to the end to download the full example code.
Maxwell 2D - Simplified ion trap modeling#
Problem description#
Surface electrodes adjacent to grating couplers render an integrated ion trap. In this workflow surface electrodes are modeled using ANSYS Maxwell and grating couplers using ANSYS Lumerical. ANSYS Maxwell electrostatic solver, based on the finite element method, allows to evaluate the ion trap height.Then the coordinates of the ion trap are passed to an optimization algorithm to define the optimal two dimensional grating coupler design, which will focus the laser beam at the ion trap height.
The workflow is explained in detail in this article: https://optics.ansys.com/hc/en-us/articles/20715978394131-Integrated-Ion-Traps-using-Surface-Electrodes-and-Grating-Couplers
The workflow consists of these steps: - Set up the Maxwell 2D parametric model. - Identify the electric field node point for each design point. - Export the node coordinates for the subsequent Lumerical step. - Launch the Lumerical scripts.
# Perform required imports
# ------------------------
import os
from pathlib import Path
import shutil
import tempfile
import time
from PIL import Image
from ansys.aedt.core import Maxwell2d
from ansys.lumerical.core import FDTD
Prepare and launch Maxwell#
Define constants.
AEDT_VERSION = os.getenv("AEDT_VERSION", "2025.2") # Set your AEDT version here
NUM_CORES = 4
NG_MODE = (
os.getenv("ON_CI", "false").lower() == "true"
) # Open AEDT UI when it is launched, unless running in CI
NODE_FILENAME = "NodePositionTable.tab"
LEGEND_FILENAME = "legend.txt"
PARENT_DIR_PATH = Path(__file__).parent.absolute()
# Create temporary directory.
temp_folder = tempfile.TemporaryDirectory(suffix=".ansys")
lumerical_script_folder = Path(temp_folder.name) # / "lumerical_scripts"
node_path = lumerical_script_folder / NODE_FILENAME
legend_path = lumerical_script_folder / LEGEND_FILENAME
# Launch AEDT and start a Maxwell 2D design.
project_name = os.path.join(temp_folder.name, "IonTrapMaxwell.aedt")
m2d = Maxwell2d(
project=project_name,
design="01_IonTrap_3binary2D",
solution_type="Electrostatic",
version=AEDT_VERSION,
non_graphical=NG_MODE,
new_desktop=True,
)
m2d.modeler.model_units = "um"
PyAEDT INFO: Python version 3.12.7 (tags/v3.12.7:0b05ead, Oct 1 2024, 03:06:41) [MSC v.1941 64 bit (AMD64)].
PyAEDT INFO: PyAEDT version 0.17.3.
PyAEDT INFO: Initializing new Desktop session.
PyAEDT INFO: Log on console is enabled.
PyAEDT INFO: Log on file C:\Users\ansys\AppData\Local\Temp\pyaedt_ansys_06eaa3fa-58b0-40f8-9d87-5ff49b1a68c0.log is enabled.
PyAEDT INFO: Log on AEDT is disabled.
PyAEDT INFO: Debug logger is disabled. PyAEDT methods will not be logged.
PyAEDT INFO: Launching PyAEDT with gRPC plugin.
PyAEDT INFO: New AEDT session is starting on gRPC port 56676.
PyAEDT INFO: Electronics Desktop started on gRPC port: 56676 after 11.014288902282715 seconds.
PyAEDT INFO: AEDT installation Path C:\Program Files\ANSYS Inc\v252\AnsysEM
PyAEDT INFO: Ansoft.ElectronicsDesktop.2025.2 version started with process ID 5428.
PyAEDT INFO: Project IonTrapMaxwell has been created.
PyAEDT INFO: Added design '01_IonTrap_3binary2D' of type Maxwell 2D.
PyAEDT INFO: Aedt Objects correctly read
PyAEDT INFO: Modeler2D class has been initialized!
PyAEDT INFO: Modeler class has been initialized! Elapsed time: 0m 0sec
Preprocess#
The preprocessing is performed using the following steps: 1. Define design variables. 2. Create design geometry. 3. Define excitations. 4. (Optional) Define mesh settings.
# Initialize dictionaries for design variables.
geom_params = {
"div": str(73 / 41),
"w_rf": "41um",
"w_dc": "41um*div",
"w_cut": "4um",
"metal_thickness": "1um",
"offset_glass": "50um",
"glass_thickness": "10um",
"x_dummy": "2um",
"y_dummy": "300um",
}
# Define design variables from dictionaries
for k, v in geom_params.items():
m2d[k] = v
# Create design geometry
dc = m2d.modeler.create_rectangle(
origin=["-w_dc/2", "-metal_thickness/2", "0"],
sizes=["w_dc", "metal_thickness", 0],
name="DC",
material="aluminum",
)
# dc.color = (0, 0, 255) # rgb
gnd = m2d.modeler.create_rectangle(
origin=["-(w_dc/2+w_cut+w_rf+offset_glass)", "-(metal_thickness/2+glass_thickness)", "0"],
sizes=["2*(w_dc/2+w_cut+w_rf+offset_glass)", "-metal_thickness", 0],
name="gnd",
material="aluminum",
)
rf = m2d.modeler.create_rectangle(
origin=["-(w_dc/2+w_cut+w_rf)", "-metal_thickness/2", "0"],
sizes=["w_rf", "metal_thickness", 0],
name="RF",
material="aluminum",
)
sub_glass = m2d.modeler.create_rectangle(
origin=["-(w_dc/2+w_cut+w_rf+offset_glass)", "-metal_thickness/2", "0"],
sizes=["2*(w_dc/2+w_cut+w_rf+offset_glass)", "-glass_thickness", 0],
name="RF",
material="glass",
)
ins = m2d.modeler.create_rectangle(
origin=["-(w_dc/2+w_cut)", "-metal_thickness/2", "0"],
sizes=["w_cut", "metal_thickness", 0],
name="ins",
material="vacuum",
)
# Create dummy objects for mesh and center line for postprocessing and region
dummy = m2d.modeler.create_rectangle(
origin=["0", "metal_thickness/2", "0"],
sizes=["-x_dummy", "y_dummy", 0],
name="dummy",
material="vacuum",
)
# Create region
region = m2d.modeler.create_region(
pad_value=[100, 0, 100, 0], pad_type="Absolute Offset", name="Region"
)
# Create the center line for electric field maximum point identification
center_line = m2d.modeler.create_polyline(
points=[["0", "metal_thickness/2", "0"], ["0", "metal_thickness/2+200um", "0"]],
name="center_line",
)
# Define excitations
m2d.assign_voltage(assignment=gnd.id, amplitude=0, name="ground")
m2d.assign_voltage(assignment=dc.id, amplitude=0, name="V_dc")
m2d.assign_voltage(assignment=rf.id, amplitude=1, name="V_rf")
# Define mesh settings
# For good quality results, uncomment the following mesh operations lines
#
# m2d.mesh.assign_length_mesh(
# assignment=center_line.id,
# maximum_length=1e-7,
# maximum_elements=None,
# name="center_line_0.1um",
# )
# m2d.mesh.assign_length_mesh(
# assignment=dummy.name, maximum_length=2e-6, maximum_elements=1e6, name="dummy_2um"
# )
# m2d.mesh.assign_length_mesh(
# assignment=ins.id,
# maximum_length=8e-7,
# inside_selection=False,
# maximum_elements=1e6,
# name="ins_0.8um",
# )
# m2d.mesh.assign_length_mesh(
# assignment=[dc.id, rf.id],
# maximum_length=5e-6,
# inside_selection=False,
# maximum_elements=1e6,
# name="dc_5um",
# )
# m2d.mesh.assign_length_mesh(
# assignment=gnd.id,
# maximum_length=1e-5,
# inside_selection=False,
# maximum_elements=1e6,
# name="gnd_10um",
# )
# Duplicate structures and assignments to complete the model
m2d.modeler.duplicate_and_mirror(
assignment=[rf.id, dummy.id, ins.id],
origin=["0", "0", "0"],
vector=["-1", "0", "0"],
duplicate_assignment=True,
)
PyAEDT INFO: Materials class has been initialized! Elapsed time: 0m 0sec
PyAEDT INFO: Boundary Voltage ground has been created.
PyAEDT INFO: Boundary Voltage V_dc has been created.
PyAEDT INFO: Boundary Voltage V_rf has been created.
['RF_2', 'dummy_1', 'ins_1']
Run simulation and parametric sweep#
Create, validate, and analyze setup
setup_name = "MySetupAuto"
setup = m2d.create_setup(name=setup_name)
setup.props["PercentError"] = 0.1
setup.update()
m2d.validate_simple()
m2d.analyze_setup(name=setup_name, use_auto_settings=False, cores=NUM_CORES)
# Create and solve parametric sweep
# Keeping w_rf constant, recompute the w_dc values from the desired ratios w_rf/w_dc
div_sweep_start = 1.4
div_sweep_stop = 2
sweep = m2d.parametrics.add(
variable="div",
start_point=div_sweep_start,
end_point=div_sweep_stop,
step=0.2,
variation_type="LinearStep",
name="w_dc_sweep",
)
add_points = [1, 1.3]
for p in add_points:
sweep.add_variation(sweep_variable="div", start_point=p, variation_type="SingleValue")
sweep["SaveFields"] = True
sweep.analyze(cores=NUM_CORES)
PyAEDT INFO: Key Desktop/ActiveDSOConfigurations/Maxwell 2D correctly changed.
PyAEDT INFO: Solving design setup MySetupAuto
PyAEDT INFO: Key Desktop/ActiveDSOConfigurations/Maxwell 2D correctly changed.
PyAEDT INFO: Design setup MySetupAuto solved correctly in 0.0h 0.0m 9.0s
PyAEDT INFO: Parsing C:/Users/ansys/AppData/Local/Temp/tmp3_tyrb15.ansys/IonTrapMaxwell.aedt.
PyAEDT INFO: File C:/Users/ansys/AppData/Local/Temp/tmp3_tyrb15.ansys/IonTrapMaxwell.aedt correctly loaded. Elapsed time: 0m 0sec
PyAEDT INFO: aedt file load time 0.01567864418029785
PyAEDT INFO: Key Desktop/ActiveDSOConfigurations/Maxwell 2D correctly changed.
PyAEDT INFO: Solving Optimetrics
PyAEDT INFO: Key Desktop/ActiveDSOConfigurations/Maxwell 2D correctly changed.
PyAEDT INFO: Design setup w_dc_sweep solved correctly in 0.0h 0.0m 36.0s
True
Postprocess#
Create the Ey expression in the PyAEDT advanced field calculator. Due to the symmetric nature of this specific geometry, the electric field node will be located along the center line. The electric field node is the point where the Ey will be zero and can be found directly by Maxwell postprocessing features.
e_line = m2d.post.fields_calculator.add_expression(calculation="e_line", assignment=None)
my_plots = m2d.post.fields_calculator.expression_plot(
calculation="e_line", assignment="center_line", names=[e_line]
)
my_plots[1].edit_x_axis_scaling(min_scale="20um", max_scale="200um")
my_plots[1].update_trace_in_report(
my_plots[1].get_solution_data().expressions, variations={"div": ["All"]}, context="center_line"
)
# Identify the zero point for each trace
my_plots[1].add_cartesian_y_marker("0")
my_plots[1].add_trace_characteristics(
"XAtYVal", arguments=["0"], solution_range=["Full", "20", "200"]
)
# Export the points at which Ey=0 to a TXT file
my_plots[1].export_table_to_file(my_plots[1].plot_name, str(node_path), table_type="Legend")
PyAEDT INFO: PostProcessor class has been initialized! Elapsed time: 0m 0sec
PyAEDT INFO: PostProcessor class has been initialized! Elapsed time: 0m 0sec
PyAEDT INFO: Post class has been initialized! Elapsed time: 0m 0sec
PyAEDT INFO: Solution Data Correctly Loaded.
True
Prepare and run Lumerical simulation#
Edit the file outputted by Maxwell to be read in by Lumerical
new_line = []
with open(node_path, "r", encoding="utf-8") as f:
lines = f.readlines()
new_line.append(lines[0])
for line in lines[1:]:
new_line.append(line.split("\t")[0])
new_line.append("\n" + line.split("\t")[1].lstrip())
with open(legend_path, "w", encoding="utf-8") as f:
for line in new_line:
f.write(line)
# Copy Lumerical scripts and illustration to the local folder
gc_farfiled_path = shutil.copy(PARENT_DIR_PATH / "GC_farfield.lsf", lumerical_script_folder)
gc_opt_path = shutil.copy(PARENT_DIR_PATH / "GC_Opt.lsf", lumerical_script_folder)
read_data_path = shutil.copy(PARENT_DIR_PATH / "Readata.lsf", lumerical_script_folder)
img_path = shutil.copy(PARENT_DIR_PATH / "img_001.jpg", lumerical_script_folder)
# Start the Lumerical process
gc_0 = FDTD(gc_opt_path)
# Run the first script: build geometry and run optimization
gc_1 = FDTD(read_data_path)
print(
"Optimize for the nodal point located",
str(gc_1.getv("T5")),
"um, above the linearly apodized grating coupler",
)
# Run the optimized design
gc_2 = FDTD(str(lumerical_script_folder / "Testsim_Intensity_best_solution"))
gc_2.save(str(lumerical_script_folder / "GC_farfields_calc"))
gc_2.run()
# Run the second script for calculating plots
gc_2.feval(gc_farfiled_path)
print(f"Target focal distance of output laser beam: {gc_2.getv('Mselect') * 1000000} (um)")
print(f"Actual focal distance for the optimised geometry: {gc_2.getv('Mactual') * 1000000} (um)")
print(f"Relative error: {gc_2.getv('RelVal') * 100}%")
print(f"FWHM of vertical direction at focus: {gc_2.getv('FWHM_X') * 1000000} (um)")
print(f"FWHM of horizontal direction at focus {gc_2.getv('FWHM_Y') * 1000000} (um)")
print(f"Substrate material : {gc_2.getv('Material')}")
print(f"Waveguide etch depth: {gc_2.getv('GC_etch') * 1000000000} (nm)")
print(f"Grating period (P): {gc_2.getv('GC_period') * 1000000000} (nm)")
print(f"Grating minimum duty cycle: {gc_2.getv('GC_DCmin')}")
# Display grating schema image
def in_ipython():
try:
from IPython import get_ipython
return get_ipython() is not None
except ImportError:
return False
schema_img = Image.open(PARENT_DIR_PATH / "img_001.jpg")
# Show image inside IPython / Jupyter
if in_ipython():
from IPython.display import display
display(schema_img)
# Show image using default image viewer
else:
schema_img.show()
schema_img.close()
Optimize for the nodal point located 81.2853 um, above the linearly apodized grating coupler
Target focal distance of output laser beam: 81.28529999999999 (um)
Actual focal distance for the optimised geometry: 100.05105105105105 (um)
Relative error: 18.756175826154816%
FWHM of vertical direction at focus: 0.0 (um)
FWHM of horizontal direction at focus 24.76276276276276 (um)
Substrate material : SiO2 (Glass) - Palik
Waveguide etch depth: 300.0 (nm)
Grating period (P): 857.4081470805504 (nm)
Grating minimum duty cycle: 0.47427900264228956
Exit the solver#
Close FDTD projects and release AEDT
gc_0.close()
gc_1.close()
gc_2.close()
m2d.save_project()
m2d.desktop_class.release_desktop()
# Wait three seconds to allow AEDT to shut down before cleaning the temporary directory
time.sleep(3)
# Clean up the temporary folder
temp_folder.cleanup()
PyAEDT INFO: Project IonTrapMaxwell Saved correctly
PyAEDT INFO: Desktop has been released and closed.
Total running time of the script: (41 minutes 53.344 seconds)