From f3b07cd6fbff0d6aa1efa4850616ef339aa15254 Mon Sep 17 00:00:00 2001 From: MWit83 Date: Tue, 20 Jan 2026 14:18:57 +0100 Subject: [PATCH] Create openems_core_50ghz_mpa.py --- .../core_1/openems/openems_core_50ghz_mpa.py | 155 ++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 modules/module_2_50GHz_MPA/part_4_layout_EMsims/core_1/openems/openems_core_50ghz_mpa.py diff --git a/modules/module_2_50GHz_MPA/part_4_layout_EMsims/core_1/openems/openems_core_50ghz_mpa.py b/modules/module_2_50GHz_MPA/part_4_layout_EMsims/core_1/openems/openems_core_50ghz_mpa.py new file mode 100644 index 00000000..c64c3fa4 --- /dev/null +++ b/modules/module_2_50GHz_MPA/part_4_layout_EMsims/core_1/openems/openems_core_50ghz_mpa.py @@ -0,0 +1,155 @@ +import os +import sys +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), 'modules'))) + +import modules.util_stackup_reader as stackup_reader +import modules.util_gds_reader as gds_reader +import modules.util_utilities as utilities +import modules.util_simulation_setup as simulation_setup +import modules.util_meshlines as util_meshlines + +import os +import matplotlib.pyplot as plt # pip install matplotlib +import numpy as np +from CSXCAD import ContinuousStructure +from CSXCAD import AppCSXCAD_BIN +from openEMS import openEMS +from openEMS.physical_constants import * + + +# Model comments +# Changes for IHP Analog Academy in 01/2026: +# Changed to MUR boundary at zmax so that radiation can be absorbed. +# From experience, using MUR can create numerical issue when it touches the lossy silicon, so it is ONLY used at zmax. +# All other boundaries use PEC, for best simulation speed. +# Distance to side walls reduced, we have not much field there because of the Metal1 ground layout. + +# This model uses an alternative syntax where are settings are stored in settings dictionary, +# which makes it easier to implement future extensions without touching again the +# simulation model code + + +# ======================== workflow settings ================================ +settings = {} + +settings['preview_only'] = False # @brief Enable this to preview model/mesh only, without starting simulation +settings['postprocess_only'] = False # @brief Enable this to show existing results only, without starting simulation + +# ===================== input files and path settings ======================= + +# GDS filename +gds_filename = "../layout_gds/50_ghz_mpa_core_no_BJT.gds" # geometries +XML_filename = "SG13G2.xml" # stackup + +settings['purpose'] = [0] # @brief Which GDSII data type is evaluated? Values in [] can be separated by comma +settings['preprocess_gds'] = False # @brief Preprocess GDSII for safe handling of cutouts/holes? +settings['merge_polygon_size'] = 0.5 # @brief Merge via polygons with distance less than .. microns, set to 0 to disable via merging. + +# get path for this simulation file +script_path = utilities.get_script_path(__file__) + +# use script filename as model basename +model_basename = utilities.get_basename(__file__) + +# set and create directory for simulation output +sim_path = utilities.create_sim_path (script_path,model_basename) +print('Simulation data directory: ', sim_path) + + +# ======================== simulation settings ================================ + + +settings['unit'] = 1e-06 # @brief Geometry units, 1E-6 is in microns +settings['margin'] = 20 # @brief Distance from GDSII geometry boundary to simulation boundary, in project units + +settings['fstart'] = 0e9 # @brief start frequency [Hz] +settings['fstop'] = 350e9 # @brief stop frequency [Hz] +settings['numfreq'] = 401 # @brief number of frequency steps [Hz] + +settings['refined_cellsize'] = 0.5 # @brief mesh cell size in conductor region, in project units + +# choices for boundary: +# 'PEC' : perfect electric conductor (default) +# 'PMC' : perfect magnetic conductor, useful for symmetries +# 'MUR' : simple MUR absorbing boundary conditions +# 'PML_8' : PML absorbing boundary conditions +settings['Boundaries'] = ['PEC', 'PEC', 'PEC', 'PEC', 'PEC', 'MUR'] + +settings['cells_per_wavelength'] = 20 # @brief how many mesh cells per wavelength, must be 10 or more +settings['energy_limit'] = -50 # @brief end criteria for residual energy (dB), default is -40 + + +# ports from GDSII Data, polygon geometry from specified special layer +# note that for multiport simulation, excitations are switched on/off in simulation_setup.createSimulation below +simulation_ports = simulation_setup.all_simulation_ports() +# instead of in-plane port specified with target_layername, we here use via port specified with from_layername and to_layername. GND means bottom of simulation box +simulation_ports.add_port(simulation_setup.simulation_port(portnumber=1, voltage=1, port_Z0=50, source_layernum=201, from_layername='Metal3', to_layername='TopMetal2', direction='z')) +simulation_ports.add_port(simulation_setup.simulation_port(portnumber=2, voltage=1, port_Z0=50, source_layernum=202, from_layername='Metal3', to_layername='TopMetal2', direction='z')) +simulation_ports.add_port(simulation_setup.simulation_port(portnumber=3, voltage=1, port_Z0=50, source_layernum=203, target_layername='Metal2', direction='-x')) +simulation_ports.add_port(simulation_setup.simulation_port(portnumber=4, voltage=1, port_Z0=50, source_layernum=204, target_layername='Metal2', direction='x')) + +# ======================== simulation ================================ + +# get technology stackup data +materials_list, dielectrics_list, metals_list = stackup_reader.read_substrate (XML_filename) +# get list of layers from technology +layernumbers = metals_list.getlayernumbers() +layernumbers.extend(simulation_ports.portlayers) + +# read geometries from GDSII, only purpose 0 +allpolygons = gds_reader.read_gds(gds_filename, + layernumbers, + purposelist=settings['purpose'], + metals_list=metals_list, + preprocess=settings['preprocess_gds'], + merge_polygon_size=settings['merge_polygon_size']) + + +########### create model, run and post-process ########### + +settings['simulation_ports'] = simulation_ports +settings['materials_list'] = materials_list +settings['dielectrics_list'] = dielectrics_list +settings['metals_list'] = metals_list +settings['layernumbers'] = layernumbers +settings['allpolygons'] = allpolygons +settings['sim_path'] = sim_path +settings['model_basename'] = model_basename + +# define excitation and stop criteria and boundaries +FDTD = openEMS(EndCriteria=np.exp(settings['energy_limit']/10 * np.log(10))) +FDTD.SetGaussExcite((settings['fstart'] + settings['fstop'])/2, + (settings['fstop'] - settings['fstart'])/2) +FDTD.SetBoundaryCond(settings['Boundaries']) + + +########### create model, run and post-process ########### + +# run all port excitations, one after another +for port in simulation_ports.ports: + settings['excite_portnumbers'] = [port.portnumber] + + # prepare model from GDSII data + simulation_setup.setupSimulation(FDTD=FDTD, settings=settings) # must use named parameters when using settings dict! + + # preview model and start simulation + simulation_setup.runSimulation(FDTD=FDTD, settings=settings) # must use named parameters when using settings dict! + + +# Initialize an empty matrix for S-parameters +num_ports = simulation_ports.portcount +s_params = np.empty((num_ports, num_ports, settings['numfreq']), dtype=object) + +# Define frequency resolution (postprocessing) +f = np.linspace(settings['fstart'], settings['fstop'], settings['numfreq']) + +# Populate the S-parameter matrix with simulation results +for i in range(1, num_ports + 1): + for j in range(1, num_ports + 1): + s_params[i-1, j-1] = utilities.calculate_Sij(i, j, f, sim_path, simulation_ports) + +# Write to Touchstone *.snp file +snp_name = os.path.join(sim_path, model_basename + '.s' + str(num_ports) + 'p') +utilities.write_snp(s_params, f, snp_name) + +print('Created S-parameter output file at ', snp_name)