diff --git a/.github/workflows/maxwell2d-lumerical.yml b/.github/workflows/maxwell2d-lumerical.yml new file mode 100644 index 000000000..1b90cee80 --- /dev/null +++ b/.github/workflows/maxwell2d-lumerical.yml @@ -0,0 +1,116 @@ +name: Maxwell2D Lumerical Workflow + +on: + workflow_dispatch: + inputs: + doc-build: + required: false + default: false + type: boolean + description: 'Whether to build the documentation' + workflow_call: + inputs: + doc-build: + required: false + default: false + type: boolean + description: 'Whether to build the documentation' + push: + branches: + - main + pull_request: + paths: + - 'maxwell2d-lumerical/**' + +env: + ANSYSLMD_LICENSE_FILE: ${{ format('1055@{0}', secrets.LICENSE_SERVER )}} + ANSYS_RELEASE_FOR_DOCS: 25.2.0 + # AWP_ROOT252: 'C:\Program Files\ANSYS Inc\v252' + MAIN_PYTHON_VERSION: '3.12' + # ON_CI: true + RUN_DOC_BUILD: false + +jobs: + + maxwell2d-lumerical: + name: Maxwell2D Lumerical + runs-on: [self-hosted, Windows, pyansys-workflows] + strategy: + fail-fast: false + matrix: + ansys-release: [25.1, 25.2] + steps: + - name: Checkout code + uses: actions/checkout@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493 # v4.2.2 + with: + sparse-checkout: | + maxwell2d-lumerical + doc + + - name: Set up Python ${{ env.MAIN_PYTHON_VERSION }} + uses: actions/setup-python@2e3e4b15a884dc73a63f962bff250a855150a234 # v5.6.0 + with: + python-version: ${{ env.MAIN_PYTHON_VERSION }} + + # - name: Set up environment variables + # shell: bash + # run: | + # VERSION="${{ matrix.ansys-release }}" + # ANSYS_RELEASE_SHORT="${VERSION%.*}" + # echo "ANSYS_RELEASE_SHORT=$ANSYS_RELEASE_SHORT" >> $GITHUB_ENV + # ANSYS_RELEASE_COMPACT="${ANSYS_RELEASE_SHORT//./}" + # echo "ANSYS_RELEASE_COMPACT=$ANSYS_RELEASE_COMPACT" >> $GITHUB_ENV + + - name: Install dependencies + shell: bash + run: | + python -m pip install --upgrade pip + python -m venv .venv + source .venv/Scripts/activate + pip install -r maxwell2d-lumerical/requirements_${{ matrix.ansys-release }}.txt + + - name: Run the workflow script + shell: bash + run: | + source .venv/Scripts/activate + python maxwell2d-lumerical/wf_ml_01_ion_trap_modelling.py + + - name: (DOCS) Check if docs should be built + if: (github.event_name == 'workflow_dispatch' || github.event_name == 'schedule') && inputs.doc-build + shell: bash + run: | + echo "Requested to build docs..." + if [ "${{ matrix.ansys-release }}" = "$ANSYS_RELEASE_FOR_DOCS" ]; then + echo "Building docs" + echo "RUN_DOC_BUILD=true" >> $GITHUB_ENV + else + echo "Not building docs - since not primary release" + echo "RUN_DOC_BUILD=false" >> $GITHUB_ENV + fi + + - name: (DOCS) Build the documentation (only on ${{ env.ANSYS_RELEASE_FOR_DOCS}}) + if: ${{ env.RUN_DOC_BUILD == 'true' }} + env: + BUILD_DOCS_SCRIPT: 'maxwell2d-lumerical/wf_ml_01_ion_trap_modelling.py' + shell: bash + run: | + .venv/Scripts/activate + cd doc + pip install -r requirements.txt + ./make.bat html + + - name: (DOCS) Adapt the documentation paths + if: ${{ env.RUN_DOC_BUILD == 'true' }} + shell: bash + run: | + find . -type f -exec sed -i 's|C:\\Users\\ansys\\actions-runner\\_work\\pyansys-workflows\\pyansys-workflows\\doc\\source\\examples\\maxwell2d-lumerical\\images\\|./images/|g' {} + + + - name: (DOCS) Upload docs artifacts + if: ${{ env.RUN_DOC_BUILD == 'true' }} + uses: actions/upload-artifact@v4 + with: + name: maxwell2d-lumerical-docs + path: | + doc/_build/ + doc/source/examples/maxwell2d-lumerical/ + overwrite: true diff --git a/README.md b/README.md index 4a33c8bf8..2e1d77512 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ for every part of the simulation process. The available workflows are: - For geometry: Ansys SpaceClaim / Ansys Discovery / Ansys Geometry Service - For meshing: Ansys Fluent Meshing - For simulation: Ansys Fluent Solver + - [Geometry, mechanical and post-processing](https://github.com/ansys/pyansys-workflows/tree/main/geometry-mechanical-dpf): this workflow demonstrates how to create a printed circuit board (PCB) geometry, mesh, run steady state and transient thermal analysis, and post-process using DPF. The geometry generated is a simple PCB with multiple chips. @@ -35,10 +36,12 @@ for every part of the simulation process. The available workflows are: - For geometry: Ansys SpaceClaim / Ansys Discovery / Ansys Geometry Service - For simulation: Ansys Mechanical - For post-procesing: Ansys Data Processing Framework + - [Fluent and mechanical analysis](https://github.com/ansys/pyansys-workflows/tree/main/fluent-mechanical): this workflow demonstrates how to perform a Conjugate Heat Transfer (CHT) analysis for an exhaust manifold to simulate heat transfer between solid and fluid domains, calculate heat transfer coefficients (HTCs) and temperature distribution, and export results for thermo-mechanical analysis. The thermo-mechanical assessment is then performed to evaluate the exhaust manifold's performance under thermal cycling, aiding in design optimization for durability The involved Ansys products are: - For fluids analysis: Ansys Fluent - For thermal analysis: Ansys Mechanical + - [Speos and optiSLang robustness analysis](https://github.com/ansys/pyansys-workflows/tree/main/speos-optislang): this workflow performs a robustness study to evaluate how variations in LED source power and position influence lightguide performance using PySpeos and PyOptiSLang. The analysis quantifies performance through key metrics such as RMS contrast, average luminance, and the number of failed regulations. @@ -46,6 +49,8 @@ for every part of the simulation process. The available workflows are: - For optical analysis: Ansys Speos - For robustness analysis: Ansys optiSLang +- [Maxwell2D and Lumerical ion trap modelling](https://github.com/ansys/pyansys-workflows/tree/main/maxwell2d-lumerical): this workflow ... + ## How to run the workflows All workflows are structured in the same way, with a Python script for each part of the simulation process. diff --git a/doc/source/conf.py b/doc/source/conf.py index b9a24a9f5..c681c3f42 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -139,6 +139,7 @@ def examples_gallery_dirs_and_filename_pattern(): "../../geometry-mesh", "../../geometry-mesh-fluent", "../../speos-optislang", + "../../maxwell2d-lumerical", ] gallery_dirs = [ "examples/fluent-mechanical", @@ -146,6 +147,7 @@ def examples_gallery_dirs_and_filename_pattern(): "examples/geometry-mesh", "examples/geometry-mesh-fluent", "examples/speos-optislang", + "examples/maxwell2d-lumerical", ] return examples_dirs, gallery_dirs, filename_pattern diff --git a/doc/source/examples.rst b/doc/source/examples.rst index daab7737b..5446adeb9 100644 --- a/doc/source/examples.rst +++ b/doc/source/examples.rst @@ -32,3 +32,7 @@ Workflow examples .. include:: examples/speos-optislang/index.rst :start-line: 1 :end-before: .. toctree + +.. include:: examples/maxwell2d-lumerical/index.rst + :start-line: 1 + :end-before: .. toctree diff --git a/maxwell2d-lumerical/.gitignore b/maxwell2d-lumerical/.gitignore new file mode 100644 index 000000000..e6d35e74c --- /dev/null +++ b/maxwell2d-lumerical/.gitignore @@ -0,0 +1 @@ +outputs \ No newline at end of file diff --git a/maxwell2d-lumerical/GC_Opt.lsf b/maxwell2d-lumerical/GC_Opt.lsf new file mode 100644 index 000000000..ed5df0a13 --- /dev/null +++ b/maxwell2d-lumerical/GC_Opt.lsf @@ -0,0 +1,301 @@ +# This script file builds the simulation file for the apodized grating coupler, GC +# entirely using script commands, with the goal to focus the output laser beam at a specific distance. Objects are added to the simulation file +# using the specific add commands. Then the script reads the coordinates of the nodal points calculated from Maxwell +# and sets the 5th solution as the target distance of the foccused beam. An optimization is created based on the define FoM in the analysis group & uses as +# as optimization parameters, the pitch, the etch depth and the minimum duty cycle of the GC. + +clear; +switchtolayout; + +selectall; deleteall; # select and delete all objects to make sure we start with a clean project file +setprofile = 0; # not add the profile monitor for the first run; 1 to set this monitor +um=1e-6; + +#################################### add simulation region/mesh/source ############################## +addfdtd; +set("simulation time", 3000e-15); # the unit is second +set("dimension", "2D"); +set("x",16*um); +set("y",-0.75*um); +set("z",0*um); +set("x span", 48*um); +set("y span", 3.5*um); +set("mesh accuracy", 2); + +addmode; #addsource +set("injection axis","x"); +set("x",-3*um); +set("y",-1*um); +set("y span",1*um); +set("z",0); +set("z span",1.14*um); +set("wavelength start", 1550e-9); +set("wavelength stop", 1550e-9); + +adddftmonitor; #addmonitor +set("name","near_field"); +set("monitor type",6); +set("x",24e-6); set("x span",60e-6); +set("y",0.7e-6); +set("z",0e-6); set("z span",15.85e-6); + +############################################ Define geometry ############################################## +# add structures + + +########### Scripted part of the Structure Group ########### +addstructuregroup; # add an apodised grating +set("name","GC_2D"); +set("x",0); # sets the x position + +set("y",-1.1*um); + +set("z",0*um); +adduserprop("index",0,2); +adduserprop("sidewall_angle",0,90); +adduserprop("duty cycle",0,0.8); +adduserprop("n_uniform_gratings",0,1); +adduserprop("dc_min",0,0.476); +adduserprop("dc_uniform",0,0.85); +adduserprop("n_apodized_gratings",0,30); +adduserprop("target length",0,50); +adduserprop("h total",2,0.3*um); +adduserprop("etch depth",2,0.3*um); +adduserprop("input length",2,10*um); +adduserprop("output length",2,80*um); +adduserprop("pitch_uniform",2,0.761*um); +adduserprop("material",5,""); +set('script',' + +deleteall; +fill_width_uniform = pitch_uniform*dc_uniform; +etch_width_uniform = pitch_uniform*(1-dc_uniform); + + +etch_width_apod = linspace(dc_uniform, dc_min, n_apodized_gratings); + +n_periods = ceil(%target length%/pitch_uniform); +fill_width = pitch_uniform*dc_uniform; +etch_width = pitch_uniform*(1-dc_uniform); +L = n_periods*pitch_uniform + etch_width; +sidewall_angle_rad = (90-sidewall_angle)*pi/180; + + +if(%etch depth% > %h total%) { + %etch depth% = %h total%; +} + +# input waveguide +vtx = [-%input length%,%h total%;0,%h total%;%h total%*tan(sidewall_angle_rad),0;-%input length%-%h total%*tan(sidewall_angle_rad),0]; # microns +addpoly; +set("name","input waveguide"); +set("vertices", vtx); +set("x", 0); +set("y", 0); +set("material",material); +if(get("material")=="") +{ set("index",index); } +set("override mesh order from material database", 1); +set("mesh order", 3); + +# lower layer below grating +if(%etch depth% < %h total%) { + addrect; + set("name","lower layer"); + set("x min",-%input length%); + set("x max",%output length%); + set("y min",0); + set("y max",%h total%-%etch depth%); + set("material",material); + if(get("material")=="") + { set("index",index); } +} + +#add grating +for(i=1:(n_uniform_gratings+n_apodized_gratings)){ + + if (i<=n_uniform_gratings){ + vtx = [pitch_uniform*(i-1)+etch_width_uniform,%h total%;pitch_uniform*i,%h total%;pitch_uniform*i+%etch depth%*tan(sidewall_angle_rad),%h total%-%etch depth%;pitch_uniform*(i-1)+etch_width_uniform-%etch depth%*tan(sidewall_angle_rad),%h total%-%etch depth%]; # microns + addpoly; + set("name","grating"); + set("vertices", vtx); + set("x", 0); + set("y", 0); + set("material",material); + if(get("material")=="") + { set("index",index); } + }else{ + + ew = pitch_uniform*(1-etch_width_apod(i-n_uniform_gratings)); + vtx = [pitch_uniform*(i-1)+ew,%h total%;pitch_uniform*i,%h total%;pitch_uniform*i+%etch depth%*tan(sidewall_angle_rad),%h total%-%etch depth%;pitch_uniform*(i-1)+ew-%etch depth%*tan(sidewall_angle_rad),%h total%-%etch depth%]; # microns + addpoly; + set("name","grating"); + set("vertices", vtx); + set("x", 0); + set("y", 0); + set("material",material); + if(get("material")=="") + { set("index",index); } + } + +} + +selectall; + +set("z",0); +set("z span",1e-6); + +'); + + + + +addrect; # add +set("name","cladding"); +set("x",19.5*um); # sets the x position +set("x span",121*um); # sets the x position +set("y",-0.3*um); +set("y span",1.6*um); +set("z",0*um); +set("z span",1*um); +set("material", "SiO2 (Glass) - Palik"); # material name has to be exact +set("override mesh order from material database", true); +set("mesh order", 5); # material name has to be exact + +# add structures +addrect; # add a +set("name","BOx"); +set("x",19.5*um); # sets the x position +set("x span",121*um); # sets the x position +set("y",-1.55*um); +set("y span",0.9*um); +set("z",0*um); +set("z span",1*um); +set("material", "SiO2 (Glass) - Palik"); # material name has to be exact +#set("override mesh order from material database", true); +#set("mesh order", 5); # material name has to be exact + +# add structures +addrect; # add a +set("name","Substrate"); +set("x",19.5*um); # sets the x position +set("x span",121*um); # sets the x position +set("y",-6*um); +set("y span",8*um); +set("z",0*um); +set("z span",1*um); +set("material", "SiO2 (Glass) - Palik"); # material name has to be exact +set("override mesh order from material database", true); +set("mesh order", 1); # material name has to be exact + +## Read Data from ASCII file ## +cd(filedirectory(currentscriptname)); +M=readdata("legend.txt"); +Mselect=M(5,1)*1e-6; + +################Analysis Group####################### + ## Figure of Merit --- Focus Beam ## +############################################## +addanalysisgroup; +set("name","FoM_beam"); +set("x", 0); +set("y", 0); +set("z", 0); + +addanalysisresult("FoM"); +addanalysisprop("Mselect",2,M(5,1)*um); +########### Scripted part of the Analysis Group ########### + +set('analysis script',' + +# Define far field position vector + +x=linspace(-40-06,-5e-06,800); +y=linspace(Mselect-5e-06,Mselect+5e-06,800); + +# Do far field projection +E_H_far=farfieldexact2d("nearfield_profile",x,y,{"field":"E and H"}); +E_far = E_H_far.E; +H_far = E_H_far.H; + + +Ex=E_far(:,:,:,:,1); +Ey=E_far(:,:,:,:,2); +Ez=E_far(:,:,:,:,3); + +Hx=H_far(:,:,:,:,1); +Hy=H_far(:,:,:,:,2); +Hz=H_far(:,:,:,:,3); + +E2_far=(pinch(abs(Ex)))^2+(pinch(abs(Ey)))^2+(pinch(abs(Ez)))^2; +H2_far=(pinch(abs(Hx)))^2+(pinch(abs(Hy)))^2+(pinch(abs(Hz)))^2; + +#FoM=integrate(E2_far,1:2,x,y); + +ind2 = find(y,Mselect); +A=pinch(E2_far,2,ind2); +FoM=max(A); + +'); + +adddftmonitor; +set("name","nearfield_profile"); +set("monitor type",6); # 2D y-normal +set("x",26*um); +set("x span",65*um); +set("y",0.7*um); +set("z",0); +addtogroup("FoM_beam"); + +########################## Save model ######################## + +save("Testsim"); + +########################## add Optimization sweep ######################## + +addsweep(1); +setsweep("optimization", "name", "Intensity"); +setsweep("Intensity", "Type", "Maximize"); +setsweep("Intensity", "algorithm", "Particle Swarm"); +setsweep("Intensity", "maximum generations", 5); +setsweep("Intensity", "generation size", 8); +setsweep("Intensity", "tolerance", 0); + +# define the grating pitch size +para1 = struct; +para1.Parameter = "::model::GC_2D::pitch_uniform"; +para1.Type = "Length"; +para1.Min = 0.7e-6; +para1.Max = 0.9e-6; +para1.Units = "microns"; +addsweepparameter("Intensity", para1); + +# define the grating etch depth +para2 = struct; +para2.Parameter = "::model::GC_2D::etch depth"; +para2.Type = "Length"; +para2.Min = 0.1e-6; +para2.Max = 0.3e-6; +para2.Units = "microns"; +addsweepparameter("Intensity", para2); + +para3 = struct; +para3.Parameter = "::model::GC_2D::dc_min"; +para3.Type = "Number"; +para3.Min = 0.1; +para3.Max = 0.6; +para3.Units = "microns"; +addsweepparameter("Intensity", para3); + +# define figure of merit +result_1 = struct; +result_1.Name = "new_result"; +result_1.Result = "::model::FoM_beam::FoM"; +result_1.Optimize = true; + +# add the figure of merits R & T to the optimization +addsweepresult("Intensity", result_1); +save("Testsim"); +## run optimization +runsweep("Intensity"); + diff --git a/maxwell2d-lumerical/GC_farfield.lsf b/maxwell2d-lumerical/GC_farfield.lsf new file mode 100644 index 000000000..d0a21c8a1 --- /dev/null +++ b/maxwell2d-lumerical/GC_farfield.lsf @@ -0,0 +1,104 @@ +## This script calculates the E and H component of the grating coupler +## and calculates the properties of the focused beam. + +clear; +# Define far field position vector + +num=500; # number of points +x = linspace(-80e-6,50e-6,num); +y = linspace(1e-6,400e-6,2*num); + +# Do far field projection + +E_H_far=farfieldexact2d("near_field",x,y,{"field":"E and H"}); +E_far = E_H_far.E; +H_far = E_H_far.H; + +# Extract electric & magnetic components from the E_H_far dataset + +Ex=E_far(:,:,:,:,1); +Ey=E_far(:,:,:,:,2); +Ez=E_far(:,:,:,:,3); + +Hx=H_far(:,:,:,:,1); +Hy=H_far(:,:,:,:,2); +Hz=H_far(:,:,:,:,3); + +E2_far=(pinch(abs(Ex)))^2+(pinch(abs(Ey)))^2+(pinch(abs(Ez)))^2; +H2_far=(pinch(abs(Hx)))^2+(pinch(abs(Hy)))^2+(pinch(abs(Hz)))^2; +image(x*1e6,y*1e6, E2_far,"x-coordinate (um)","y-coordinate (um)","|E|^2"); + + +cd(filedirectory(currentscriptname)); +## Read Data from ASCII file ## + M=readdata("legend.txt"); + Mselect=M(5,1)*1e-6; + +# farfield along x axis +E3_z = farfieldexact2d('near_field',x,Mselect); +E3_z = pinch(sum(abs(E3_z)^2,3)); +plot(x*1e6,E3_z,'x (um)','|E|^2 (V^2/m^2)','Line projection at y='+num2str(floor(Mselect*1e6),"%2d")+'um','linewidth=2'); +legend(""); + +# find FWHM along x axis +ind1 = find(E3_z,max(E3_z)); +xmax = x(ind1); +indR1 = find(E3_z,E3_z(ind1)/2); +if ( ind1indR2 ) { + indR2= ind2 + (ind2-indR2); + } +indL2 = abs(ind2-(abs(indR2-ind2))); +FWHM_Y=abs(y(indR2)-y(indL2)); + +##Calculate deviation between actual and target coordinates of the trap ## +#Get the length of the properties +nx = length(x); +ny = length(y); +#Get the intensity values +e2 = E2_far; +#Get the maximum intensity value +indexE2_far = find(e2, max(e2)); +#Create the grids that will be used to extract the actual position values +X = meshgridx(x,y); +Y = meshgridy(x,y); +?Mselect; +Mactual=Y(indexE2_far); +RelVal=(abs(Mselect-Mactual)/Mactual); + +# Print geometric properties in the code editor +select("Substrate"); +Material = get("material"); +select("BOx"); +BOX = get("material"); +select("GC_2D"); +GC_period = get("pitch_uniform"); +GC_etch = get("etch depth"); +GC_DCmin = get("dc_min"); + + + + + + + + + + + + diff --git a/maxwell2d-lumerical/README.rst b/maxwell2d-lumerical/README.rst new file mode 100644 index 000000000..0dc866b5f --- /dev/null +++ b/maxwell2d-lumerical/README.rst @@ -0,0 +1,4 @@ +Maxwell2D and Lumerical +======================= + +Below is an example of a geometry and meshing workflow PyAEDT and Lumerical APIS diff --git a/maxwell2d-lumerical/Readata.lsf b/maxwell2d-lumerical/Readata.lsf new file mode 100644 index 000000000..ecc62f529 --- /dev/null +++ b/maxwell2d-lumerical/Readata.lsf @@ -0,0 +1,6 @@ +cd(filedirectory(currentscriptname)); +## Read Data from ASCII file ## + M=readdata("legend.txt"); + + + T1=M(1,1); T2=M(2,1); T3=M(3,1); T4=M(4,1); T5=M(5,1); T6=M(6,1); T7=M(7,1); \ No newline at end of file diff --git a/maxwell2d-lumerical/img_001.jpg b/maxwell2d-lumerical/img_001.jpg new file mode 100644 index 000000000..a243ad078 Binary files /dev/null and b/maxwell2d-lumerical/img_001.jpg differ diff --git a/maxwell2d-lumerical/requirements_25.1.txt b/maxwell2d-lumerical/requirements_25.1.txt new file mode 100644 index 000000000..7827613d4 --- /dev/null +++ b/maxwell2d-lumerical/requirements_25.1.txt @@ -0,0 +1,3 @@ +pyaedt[all]==0.17.3 +Pillow==12.0.0 +ansys-api-lumerical==0.1.0 \ No newline at end of file diff --git a/maxwell2d-lumerical/requirements_25.2.txt b/maxwell2d-lumerical/requirements_25.2.txt new file mode 100644 index 000000000..7827613d4 --- /dev/null +++ b/maxwell2d-lumerical/requirements_25.2.txt @@ -0,0 +1,3 @@ +pyaedt[all]==0.17.3 +Pillow==12.0.0 +ansys-api-lumerical==0.1.0 \ No newline at end of file diff --git a/maxwell2d-lumerical/wf_ml_01_ion_trap_modelling.py b/maxwell2d-lumerical/wf_ml_01_ion_trap_modelling.py new file mode 100644 index 000000000..79eeb7ce3 --- /dev/null +++ b/maxwell2d-lumerical/wf_ml_01_ion_trap_modelling.py @@ -0,0 +1,324 @@ +# Copyright (C) 2024 - 2025 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +# # Maxwell2D - Simplified IonTrap Modelling +# +# Description: +# +# First step of a multi-tool workflow: Maxwell 2D model to identify electric field node in Ion Trap +# 1. Set up the Maxwell 2D Parametric Model +# 2. Identify the Electric Field Node Point for Each Design Point +# 3. Export the Node Coordinates for the subsequent Lumerical Step +# 4. Launch the Lumerical Scripts +# +# Keywords: **Ion Trap**, **Electrostatic** + +# ## Perform imports and define constants +# +# Perform required imports. + +import os +from pathlib import Path +import tempfile +import time + +# from PIL import Image +from ansys.aedt.core import Maxwell2d +from ansys.api.lumerical.lumapi import FDTD + +# sphinx_gallery_start_ignore +# Check if the __file__ variable is defined. If not, set it. +# This is a workaround to run the script in Sphinx-Gallery. +if "__file__" not in locals(): + __file__ = Path(os.getcwd(), "wf_ml_01_ion_trap_modelling.py") +# sphinx_gallery_end_ignore + +# sys.path.append("C:\\Program Files\\Lumerical\\v251\\api\\python\\") +# sys.path.append(os.path.dirname(__file__)) # Current directory +# my_path = r"D:/2025/17_IonTrap/PyAnsys_GC_test/" # Directory where Lumerical Scripts are stored +# my_node_filename = "NodePositionTable.tab" +# my_node_filename_lum = "legend.txt" + + +# Define constants. + +AEDT_VERSION = os.getenv("AEDT_VERSION", "2025.1") # Set your AEDT version here +NUM_CORES = 4 +NG_MODE = False # Open AEDT UI when it is launched. +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 = temp_folder / "lumerical_scripts" + +# ## Launch AEDT and application +# + +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" + +# ## Preprocess +# +# 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 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 Post Processing and Region + +dummy = m2d.modeler.create_rectangle( + origin=["0", "metal_thickness/2", "0"], + sizes=["-x_dummy", "y_dummy", 0], + name="dummy", + material="vacuum", +) + +region = m2d.modeler.create_region( + pad_value=[100, 0, 100, 0], pad_type="Absolute Offset", name="Region" +) +center_line = m2d.modeler.create_polyline( + points=[["0", "metal_thickness/2", "0"], ["0", "metal_thickness/2+300um", "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, please 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, +) + +# 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 parametric sweep + +# Keeping w_rf constant, we 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) + +# ## 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 post +# processing 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="280um") +my_plots[1].update_trace_in_report( + my_plots[1].get_solution_data().expressions, variations={"div": ["All"]}, context="center_line" +) +my_plots[1].add_cartesian_y_marker("0") +my_plots[1].add_trace_characteristics( + "XAtYVal", arguments=["0"], solution_range=["Full", "20", "280"] +) +file_path = lumerical_script_folder / NODE_FILENAME +my_plots[1].export_table_to_file(my_plots[1].plot_name, str(file_path), "Legend") + +# ## Release AEDT + +m2d.save_project() +m2d.release_desktop() + +# ## Edit the outputted file to be read in by Lumerical + +new_line = [] +with open(lumerical_script_folder / NODE_FILENAME, "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(lumerical_script_folder + LEGEND_FILENAME, "w", encoding="utf-8") as f: + for line in new_line: + f.write(line) + +# ## Start the Lumerical Process + +gc_0 = FDTD( + str(PARENT_DIR_PATH / "GC_Opt.lsf") +) # Run the first script: Build geometry & Run optimization +gc_1 = FDTD(str(PARENT_DIR_PATH / "Readata.lsf")) +print( + "Optimize for the Nodal point located", + str(gc_1.getv("T5")), + "um, above the linearly apodized grating coupler", +) +gc_2 = FDTD( + str(lumerical_script_folder / "Testsim_Intensity_best_solution") +) # Run the optimized design +gc_2.save(str(lumerical_script_folder / "GC_farfields_calc")) +gc_2.run() +gc_2.feval(str(PARENT_DIR_PATH / "GC_farfield.lsf")) # Run the second script for calculating plots +print("Target focal distance of output laser beam, (um) :", str(gc_2.getv("Mselect") * 1000000)) +print( + "Actual focal distance for the optimised geometry, (um) :", str(gc_2.getv("Mactual") * 1000000) +) +print("Relative error:", str(gc_2.getv("RelVal") * 100), "(%)") +print("FWHM of vertical direction at focus, (um) ", str(gc_2.getv("FWHM_X") * 1000000)) +print("FWHM of horizontal direction at focus, (um) ", str(gc_2.getv("FWHM_Y") * 1000000)) +print("Substrate material :", str(gc_2.getv("Material"))) + +print("Waveguide etch depth, (nm) ", str(gc_2.getv("GC_etch") * 1000000000)) +print("Grating period (P), (nm) ", str(gc_2.getv("GC_period") * 1000000000)) +print("Grating minimum duty cycle:", str(gc_2.getv("GC_DCmin"))) + +# Grating_Schema = Image.open(my_path + "img_001.jpg") + +# Wait 3 seconds to allow AEDT to shut down before cleaning the temporary directory. +time.sleep(3) + +# ## Clean up +# + +temp_folder.cleanup()