Human-in-the-loop design stage. User creates the geometry/shape in CST Studio; the agent programmatically configures everything else (units, materials, boundaries, Floquet ports, monitors, mesh, solver) via the CST Python API. Supports NxN supercell array extension with unique per-atom parameters. Creates TargetConfig JSON and design.py, validates constraints, and obtains explicit user approval before advancing to CST Tuning.
This skill bridges the Research stage (literature survey) and the CST Tuning stage (automated optimization). The division of labor is:
Because design decisions have irreversible downstream consequences (wrong bounds waste hours of simulation), this stage is human-in-the-loop: every critical decision requires explicit user confirmation before proceeding.
.cst file and wants to set up the optimization
pipeline (legacy fast-path).Do not use this skill if:
literature-survey
first).configs/<project>.json and design.py already exist and the user
wants to proceed directly to tuning (use cst-tuning).research_notes.md with recommended topology, materials, and parameter
ranges from the literature survey.cst_inference available with CST Python libraries.All Python commands in this skill require the CST environment:
eval "$(conda shell.bash hook 2>/dev/null)" && conda activate cst_inference
export PYTHONPATH="E:/cst/AMD64/python_cst_libraries:D:/Claude/auto_cst"
The CST Python API uses three execution modes:
add_to_history): VBA strings recorded in project history.
History-safe, survives rebuild. Use this for all setup operations.allow_history_commands() opt-in. Use only for getters.execute_vba_code): VBA via schematic context, no history.
Use for one-off operations where history is undesired.This section provides the exact VBA patterns for every simulation setup
operation the agent needs. All patterns use Mode 1 (add_to_history) unless
noted otherwise.
import cst.interface as csti
# Connect to running CST instance (or start new)
de = csti.DesignEnvironment() # starts new CST instance
# de = csti.DesignEnvironment.connect_to_any() # attach to existing
# Create new MWS project
prj = de.new_mws()
m3d = prj.model3d
# Or open existing project
prj = de.open_project(r"C:\path\to\project.cst")
m3d = prj.model3d
# Save project
prj.save() # save to current path
prj.save_as(r"C:\path\to\new.cst") # save-as
# Mode 1: add VBA to project history (recommended for all setup)
m3d.add_to_history("caption text", vba_string)
# Mode 2 opt-in (for getters, or when history is not needed)
csti.Model3D.allow_history_commands()
# Read parameters (always direct, no history impact)
params = m3d.get_parameter_list() # returns [(name, value), ...]
# Solver control
m3d.run_solver() # synchronous
m3d.start_solver() # async
m3d.is_solver_running() # poll
Inject or update design parameters. These become parametric expressions usable in all geometry/setup fields.
# Single parameter
vba = 'StoreDoubleParameter "p", 85.0'
m3d.add_to_history("store parameter: p", vba)
# Multiple parameters
vba = "\n".join([
'MakeSureParameterExists "p", "85"',
'MakeSureParameterExists "t_m", "0.2"',
'MakeSureParameterExists "w", "8"',
'MakeSureParameterExists "gap", "2"',
])
m3d.add_to_history("define workspace parameters", vba)
Existing helper in engine/vba_builder.py:
from engine.vba_builder import build_parameter_vba
vba = build_parameter_vba({"p": 85.0, "t_m": 0.2, "w": 8.0})
Set geometry, frequency, and time units. For THz metamaterials: um / THz / ns.
vba = "\n".join([
'With Units',
' .Geometry "um"',
' .Frequency "THz"',
' .Time "ns"',
' .TemperatureUnit "Kelvin"',
' .Voltage "V"',
' .Current "A"',
' .Resistance "Ohm"',
' .Conductance "Siemens"',
' .Capacitance "F"',
' .Inductance "H"',
'End With',
])
m3d.add_to_history("define units", vba)
Valid units:
| Dimension | Values |
|---|---|
| Geometry | "nm", "um", "mm", "cm", "m", "mil", "in", "ft" |
| Frequency | "Hz", "kHz", "MHz", "GHz", "THz", "PHz" |
| Time | "fs", "ps", "ns", "us", "ms", "s" |
| Temperature | "degC", "K", "degF" |
Set the background material and calculation domain spacing. For unit cell metamaterials, use vacuum background with zero spacing (domain is defined by boundaries).
vba = "\n".join([
'With Background',
' .Type "Normal"',
' .Epsilon "1.0"',
' .Mu "1.0"',
' .XminSpace "0.0"',
' .XmaxSpace "0.0"',
' .YminSpace "0.0"',
' .YmaxSpace "0.0"',
' .ZminSpace "0.0"',
' .ZmaxSpace "0.0"',
'End With',
])
m3d.add_to_history("define background", vba)
Valid types: "Normal" (vacuum/custom), "PEC" (perfect conductor)
vba = "\n".join([
'With Material',
' .Reset',
' .Name "Polyimide"',
' .Folder ""',
' .FrqType "all"',
' .Type "Normal"',
' .MaterialUnit "Frequency", "THz"',
' .MaterialUnit "Geometry", "um"',
' .Epsilon "3.5"',
' .Mu "1"',
' .Sigma "0"',
' .TanD "0.02"',
' .TanDGiven "True"',
' .TanDModel "ConstTanD"',
' .SetConstTanDStrategyEps "AutomaticOrder"',
' .ConstTanDModelOrderEps "3"',
' .Colour "0.94", "0.82", "0.52"',
' .Wireframe "False"',
' .Transparency "0.5"',
' .Create',
'End With',
])
m3d.add_to_history("define material: Polyimide", vba)
vba = "\n".join([
'With Material',
' .Reset',
' .Name "Gold_lossy"',
' .Folder ""',
' .FrqType "all"',
' .Type "Lossy metal"',
' .MaterialUnit "Frequency", "THz"',
' .MaterialUnit "Geometry", "um"',
' .Sigma "4.1e7"',
' .Colour "0.85", "0.75", "0.25"',
' .Wireframe "False"',
' .Transparency "0"',
' .Create',
'End With',
])
m3d.add_to_history("define material: Gold_lossy", vba)
For frequency-dependent metallic response (e.g., gold in THz/IR):
vba = "\n".join([
'With Material',
' .Reset',
' .Name "Gold_Drude"',
' .Folder ""',
' .FrqType "all"',
' .Type "Normal"',
' .MaterialUnit "Frequency", "THz"',
' .MaterialUnit "Geometry", "um"',
' .EpsInfinity "1.0"',
' .DispModelEps "Drude"',
' .DispCoeff1Eps "2175.6"', # plasma frequency (THz)
' .DispCoeff2Eps "6.5"', # collision frequency (THz)
' .Mu "1"',
' .Colour "0.85", "0.75", "0.25"',
' .Wireframe "False"',
' .Create',
'End With',
])
m3d.add_to_history("define material: Gold_Drude", vba)
For 2D conductive layers:
vba = "\n".join([
'With Material',
' .Reset',
' .Name "Graphene"',
' .Folder ""',
' .FrqType "all"',
' .Type "Ohmic sheet"',
' .OhmicSheetImpedance "377"', # sheet impedance in Ohms/sq
' .Colour "0.2", "0.2", "0.2"',
' .Create',
'End With',
])
m3d.add_to_history("define material: Graphene", vba)
vba = "\n".join([
'With Material',
' .Reset',
' .Name "Silicon"',
' .Folder ""',
'End With',
'MaterialLibrary.ImportMaterial "Solids", "Silicon", "Silicon"',
])
m3d.add_to_history("import material: Silicon", vba)
Material type valid values:
"Normal", "Anisotropic", "PEC", "Lossy metal", "Corrugated wall",
"Ohmic sheet", "Tensor formula", "Nonlinear"
Dispersion model valid values (DispModelEps):
"None", "Debye1st", "Debye2nd", "Drude", "Lorentz", "General1st",
"General2nd", "General", "NonLinear2nd", "NonLinear3rd",
"NonLinearKerr", "NonLinearRaman"
Existing helper in engine/vba_builder.py:
from engine.vba_builder import build_material_vba
vba = build_material_vba("Gold_lossy", mat_type="Lossy metal", sigma=4.1e7)
vba = build_material_vba("Polyimide", mat_type="Normal", epsilon=3.5, tand=0.02)
For periodic metamaterial unit cells: unit cell boundaries on X and Y (in- plane periodicity), open or expanded open on Z (propagation direction).
vba = "\n".join([
'With Boundary',
' .Xmin "unit cell"',
' .Xmax "unit cell"',
' .Ymin "unit cell"',
' .Ymax "unit cell"',
' .Zmin "expanded open"',
' .Zmax "expanded open"',
' .Xsymmetry "none"',
' .Ysymmetry "none"',
' .Zsymmetry "none"',
' .ApplyInAllDirections "False"',
' .UnitCellFitToBoundingBox "True"',
' .UnitCellAngle "90.0"',
'End With',
])
m3d.add_to_history("define boundary conditions", vba)
When studying angular dependence (theta/phi):
vba = "\n".join([
'With Boundary',
' .Xmin "unit cell"',
' .Xmax "unit cell"',
' .Ymin "unit cell"',
' .Ymax "unit cell"',
' .Zmin "expanded open"',
' .Zmax "expanded open"',
' .PeriodicUseConstantAngles "True"',
' .SetPeriodicBoundaryAngles "theta", "phi"',
' .SetPeriodicBoundaryAnglesDirection "outward"',
' .UnitCellFitToBoundingBox "True"',
' .UnitCellAngle "90.0"',
'End With',
])
m3d.add_to_history("define boundary conditions (oblique)", vba)
Boundary type valid values:
"electric", "magnetic", "tangential", "normal", "open",
"expanded open", "periodic", "conducting wall", "unit cell"
Symmetry valid values: "none", "electric", "magnetic"
Key defaults:
| Method | Default |
|---|---|
Layer (open BC PML layers) | 4 |
Reflection (PML reflection) | 0.0001 |
MinimumDistancePerWavelength | 8 |
UnitCellAngle | 90.0 |
Floquet ports are required for unit cell boundaries. They define the excitation modes for periodic structures. Must be configured after boundary conditions.
vba = "\n".join([
'With FloquetPort',
' .Reset',
' .SetDialogTheta "0"',
' .SetDialogPhi "0"',
' .SetPolarizationIndependentOfScanAnglePhi "0.0", "False"',
' .SetSortCode "+beta/pw"',
' .SetCustomizedListFlag "False"',
' .Port "Zmin"',
' .SetNumberOfModesConsidered "2"',
' .SetDistanceToReferencePlane "0"',
' .SetUseCircularPolarization "False"',
' .Port "Zmax"',
' .SetNumberOfModesConsidered "2"',
' .SetDistanceToReferencePlane "0"',
' .SetUseCircularPolarization "False"',
'End With',
])
m3d.add_to_history("define Floquet port boundaries", vba)
Number of modes: Use "2" for basic S11/S21 (TE+TM fundamental). Increase
to "18" if higher-order diffraction or cross-polarization coupling matters.
Port valid values: "Zmin", "Zmax"
Sort code valid values:
"+beta/pw", "+beta", "-beta", "+alpha", "-alpha",
"+te", "-te", "+tm", "-tm", "+orderx", "-orderx",
"+ordery", "-ordery"
# Set simulation frequency range
vba = 'Solver.FrequencyRange "0.5", "6.0"'
m3d.add_to_history("define frequency range", vba)
For a target band of 1-5 THz, simulate 0.5 to 6.0 THz (margins for edge effects).
S-parameter monitors are automatically created when Floquet ports are defined. No additional configuration needed for basic R/T/A extraction.
vba = "\n".join([
'With Monitor',
' .Reset',
' .Name "e-field (f=3.0)"',
' .Domain "Frequency"',
' .FieldType "Efield"',
' .MonitorValue "3.0"',
' .UseSubvolume "False"',
' .Create',
'End With',
])
m3d.add_to_history("define e-field monitor: f=3.0", vba)
vba = "\n".join([
'With Monitor',
' .Reset',
' .Name "h-field (f=3.0)"',
' .Domain "Frequency"',
' .FieldType "Hfield"',
' .MonitorValue "3.0"',
' .UseSubvolume "False"',
' .Create',
'End With',
])
m3d.add_to_history("define h-field monitor: f=3.0", vba)
vba = "\n".join([
'With Monitor',
' .Reset',
' .Name "power (f=3.0)"',
' .Domain "Frequency"',
' .FieldType "Powerflow"',
' .MonitorValue "3.0"',
' .UseSubvolume "False"',
' .Create',
'End With',
])
m3d.add_to_history("define power flow monitor: f=3.0", vba)
vba = "\n".join([
'With Monitor',
' .Reset',
' .Name "loss (f=3.0)"',
' .Domain "Frequency"',
' .FieldType "Powerloss"',
' .MonitorValue "3.0"',
' .UseSubvolume "False"',
' .Create',
'End With',
])
m3d.add_to_history("define power loss monitor: f=3.0", vba)
FieldType valid values:
"Efield", "Hfield", "Powerflow", "Current", "Powerloss",
"Eenergy", "Henergy", "Farfield", "Fieldsource",
"Spacecharge", "Particlecurrentdensity"
Domain valid values: "Frequency", "Time"
# Step 1: Set mesh type
vba = "\n".join([
'With Mesh',
' .MeshType "Tetrahedral"',
' .SetCreator "High Frequency"',
'End With',
])
m3d.add_to_history("set mesh type: Tetrahedral", vba)
# Step 2: Wavelength and geometry refinement
vba = "\n".join([
'With MeshSettings',
' .SetMeshType "Tet"',
' .Set "Version", 1%',
' .Set "StepsPerWaveNear", "13"',
' .Set "StepsPerWaveFar", "4"',
' .Set "PhaseErrorNear", "0.02"',
' .Set "PhaseErrorFar", "0.02"',
' .Set "CellsPerWavelengthPolicy", "cellsperwavelength"',
' .Set "StepsPerBoxNear", "10"',
' .Set "StepsPerBoxFar", "1"',
' .Set "UseRatioLimit", "0"',
' .Set "RatioLimit", "100"',
' .Set "MinStep", "0"',
' .SetMeshType "Unstr"',
' .Set "Method", "0"',
'End With',
])
m3d.add_to_history("configure Tet mesh settings", vba)
# Step 3: Mesh properties
vba = "\n".join([
'With Mesh',
' .UsePecEdgeModel "True"',
' .SetParallelMesherMode "Tet", "maximum"',
'End With',
])
m3d.add_to_history("configure mesh properties", vba)
vba = "\n".join([
'With Mesh',
' .MeshType "HexahedralFIT"',
' .SetCreator "High Frequency"',
'End With',
])
m3d.add_to_history("set mesh type: HexahedralFIT", vba)
vba = "\n".join([
'With MeshSettings',
' .SetMeshType "Hex"',
' .Set "Version", 1%',
' .Set "StepsPerWaveNear", "20"',
' .Set "StepsPerWaveFar", "20"',
' .Set "WavelengthRefinementSameAsNear", "1"',
' .Set "StepsPerBoxNear", "20"',
' .Set "StepsPerBoxFar", "1"',
' .Set "MaxStepNear", "0"',
' .Set "MaxStepFar", "0"',
' .Set "UseRatioLimitGeometry", "1"',
' .Set "RatioLimitGeometry", "20"',
'End With',
])
m3d.add_to_history("configure Hex mesh settings", vba)
MeshType valid values:
"HexahedralFIT", "HexahedralTLM", "Tetrahedral", "Surface",
"SurfaceML", "Planar"
Recommended for narrowband targets and unit cell metamaterials with Floquet ports.
vba = "\n".join([
'Mesh.SetCreator "High Frequency"',
'',
'With FDSolver',
' .Reset',
' .SetMethod "Tetrahedral", "General purpose"',
' .OrderTet "Second"',
' .OrderSrf "First"',
' .Stimulation "All", "All"',
' .ResetExcitationList',
' .AutoNormImpedance "False"',
' .NormingImpedance "50"',
' .ModesOnly "False"',
' .ConsiderPortLossesTet "True"',
' .SetShieldAllPorts "False"',
' .AccuracyTet "1e-4"',
' .LimitIterations "False"',
' .MaxIterations "0"',
' .SetCalcBlockExcitationsInParallel "True", "True", ""',
' .StoreAllResults "False"',
' .UseHelmholtzEquation "True"',
' .LowFrequencyStabilization "True"',
' .Type "Auto"',
' .MeshAdaptionTet "True"',
' .AcceleratedRestart "True"',
' .FreqDistAdaptMode "Distributed"',
' .NewIterativeSolver "True"',
' .TDCompatibleMaterials "False"',
' .ExtrudeOpenBC "True"',
' .SetOpenBCTypeTet "Default"',
' .AddMonitorSamples "True"',
' .CalcPowerLoss "True"',
' .CalcPowerLossPerComponent "False"',
' .SetKeepSolutionCoefficients "MonitorsAndMeshAdaptation"',
' .UseDoublePrecision "False"',
' .UseDoublePrecision_ML "True"',
' .UseSensitivityAnalysis "False"',
' .RemoveAllStopCriteria "Tet"',
' .AddStopCriterion "All S-Parameters", "0.01", "2", "Tet", "True"',
' .AddStopCriterion "Reflection S-Parameters", "0.01", "2", "Tet", "False"',
' .AddStopCriterion "Transmission S-Parameters", "0.01", "2", "Tet", "False"',
' .SweepMinimumSamples "3"',
' .SetNumberOfResultDataSamples "1001"',
' .SetResultDataSamplingMode "Automatic"',
' .SweepWeightEvanescent "1.0"',
' .AccuracyROM "1e-4"',
' .AddSampleInterval "", "", "1", "Automatic", "True"',
' .AddSampleInterval "", "", "", "Automatic", "False"',
' .UseParallelization "True"',
' .MaxCPUs "1024"',
' .MaximumNumberOfCPUDevices "2"',
'End With',
])
m3d.add_to_history("define frequency domain solver parameters", vba)
Key settings:
| Parameter | Valid Values |
|---|---|
SetMethod mesh | "Hexahedral", "Tetrahedral", "Surface" |
SetMethod sweep | "General purpose", "Fast reduced order model", "Discrete samples only" |
OrderTet | "First", "Second", "Third" |
Type | "Auto", "Iterative", "Direct" |
Recommended for broadband targets. Faster for wide frequency ranges.
vba = "\n".join([
'Mesh.SetCreator "High Frequency"',
'',
'With Solver',
' .Method "Hexahedral"',
' .CalculationType "TD-S"',
' .StimulationPort "All"',
' .StimulationMode "All"',
' .SteadyStateLimit "-40"',
' .MeshAdaption "False"',
' .CalculateModesOnly "False"',
' .SParaSymmetry "False"',
' .StoreTDResultsInCache "False"',
' .FullDeembedding "False"',
' .SuperimposePLWExcitation "False"',
' .UseSensitivityAnalysis "False"',
'End With',
])
m3d.add_to_history("define time domain solver parameters", vba)
The user normally creates geometry in CST Studio, but the agent may need to create simple shapes (ground planes, spacer layers, supercell copies).
vba = "\n".join([
'With Brick',
' .Reset',
' .Name "ground_plane"',
' .Component "component1"',
' .Material "Gold_lossy"',
' .Xrange "-p/2", "p/2"',
' .Yrange "-p/2", "p/2"',
' .Zrange "0", "t_m"',
' .Create',
'End With',
])
m3d.add_to_history("define brick: component1:ground_plane", vba)
vba = "\n".join([
'With Cylinder',
' .Reset',
' .Name "resonator"',
' .Component "component1"',
' .Material "Silicon"',
' .OuterRadius "r"',
' .InnerRadius "0"',
' .Axis "z"',
' .Zrange "0", "h"',
' .Xcenter "0"',
' .Ycenter "0"',
' .Segments "0"',
' .Create',
'End With',
])
m3d.add_to_history("define cylinder: component1:resonator", vba)
vba = 'Component.New "layer1"'
m3d.add_to_history("new component: layer1", vba)
# Add (union)
vba = 'Solid.Add "component1:shape_h", "component1:shape_v"'
m3d.add_to_history("boolean add shapes", vba)
# Subtract
vba = 'Solid.Subtract "component1:base", "component1:cutout"'
m3d.add_to_history("boolean subtract shapes", vba)
# Intersect
vba = 'Solid.Intersect "component1:shape_a", "component1:shape_b"'
m3d.add_to_history("boolean intersect shapes", vba)
vba = 'Solid.Rename "component1:solid1", "ground_plane"'
m3d.add_to_history("rename block", vba)
vba = 'Solid.Delete "component1:old_shape"'
m3d.add_to_history("delete shape", vba)
vba = 'Solid.ChangeMaterial "component1:shape", "Gold_lossy"'
m3d.add_to_history("change material", vba)
vba = "\n".join([
'With Transform',
' .Reset',
' .Name "component1:shape"',
' .Vector "10", "0", "0"',
' .UsePickedPoints "False"',
' .InvertPickedPoints "False"',
' .MultipleObjects "False"',
' .GroupObjects "False"',
' .Repetitions "1"',
' .MultipleSelection "False"',
' .Transform "Shape", "Translate"',
'End With',
])
m3d.add_to_history("translate shape", vba)
Creates copies while preserving the original — key for supercell tiling:
vba = "\n".join([
'With Transform',
' .Reset',
' .Name "component1:resonator"',
' .Vector "p", "0", "0"',
' .UsePickedPoints "False"',
' .InvertPickedPoints "False"',
' .MultipleObjects "True"',
' .GroupObjects "False"',
' .Repetitions "1"',
' .MultipleSelection "False"',
' .Transform "Shape", "Translate"',
'End With',
])
m3d.add_to_history("copy shape: +x direction", vba)
When MultipleObjects "True", CST creates a copy named <original>_1 (or
_2, _3 etc. for multiple repetitions). Use Solid.Rename to give copies
meaningful names.
vba = "\n".join([
'With Transform',
' .Reset',
' .Name "component1:shape"',
' .Origin "Free"',
' .Center "0", "0", "0"',
' .Angle "0", "0", "90"',
' .MultipleObjects "True"',
' .GroupObjects "False"',
' .Repetitions "1"',
' .Transform "Shape", "Rotate"',
'End With',
])
m3d.add_to_history("rotate copy: 90 deg", vba)
vba = "\n".join([
'With Transform',
' .Reset',
' .Name "component1:shape"',
' .Origin "Free"',
' .Center "0", "0", "0"',
' .PlaneNormal "1", "0", "0"',
' .MultipleObjects "True"',
' .GroupObjects "False"',
' .Repetitions "1"',
' .Transform "Shape", "Mirror"',
'End With',
])
m3d.add_to_history("mirror copy", vba)
Transform valid values:
| Parameter | Values |
|---|---|
what | "Shape", "Port", "Mixed", "Coil", "Face" |
how | "Translate", "Rotate", "Scale", "Mirror", "Matrix" |
Origin | "ShapeCenter", "CommonCenter", "Free" |
Read S-parameters from a solved project without opening CST GUI:
import cst.results as cstres
import numpy as np
pf = cstres.ProjectFile(r"C:\path\to\project.cst", allow_interactive=False)
sparams = pf.get_3d().get_result_item(r"1D Results\S-Parameters\SZmin(1),Zmin(1)")
freq = np.array(sparams.get_xdata())
s11_complex = np.array(sparams.get_ydata())
R = np.abs(s11_complex)**2
sparams21 = pf.get_3d().get_result_item(r"1D Results\S-Parameters\SZmax(1),Zmin(1)")
s21_complex = np.array(sparams21.get_ydata())
T = np.abs(s21_complex)**2
A = 1.0 - R - T # Absorptance
vba = "\n".join([
'With ASCIIExport',
' .Reset',
' .FileName "export/s-params.txt"',
' .SetFileType "ascii"',
' .Execute',
'End With',
])
m3d.add_to_history("export ASCII data", vba)
vba = "\n".join([
'With TOUCHSTONE',
' .Reset',
' .FileName "export/result.s2p"',
' .FrequencyRange "0.5", "6.0"',
' .Impedance "50"',
' .FormatString "MA"',
' .Write',
'End With',
])
m3d.add_to_history("export Touchstone", vba)
Ask the user the following questions explicitly. Do not assume answers — the user must confirm each one.
Before we set up the CST project, I need a few details:
1. TARGET: What are you designing?
- Frequency range? (e.g., 1-5 THz)
- Absorption/reflection target? (e.g., >90% absorption)
- Response type? (broadband / narrowband / multiband)
2. TOPOLOGY: What resonator shape?
(e.g., SRR, cross-patch, MIM stack, dielectric cylinder)
3. MATERIALS:
- Metal: Gold, Copper, Aluminum, PEC, graphene?
- Dielectric: Polyimide, SiO2, Si3N4, PDMS?
4. ARRAY: Single unit cell (1x1) or supercell (NxN)?
5. GEOMETRY: Do you have an existing .cst file, or should
I create a new project for you to draw the geometry in?
Based on the answers:
If the user already has a complete .cst file:
templates/<project_name>.cst.python -c "
import cst.interface as csti
prj = csti.DesignEnvironment().open_project('<PATH>')
params = prj.model3d.get_parameter_list()
print('=== PARAMETERS ===')
for name, value in params:
print(f' {name} = {value}')
prj.close()
"
Create a new CST MWS project and set up the simulation environment. The user will add geometry later.
python -c "
import cst.interface as csti
de = csti.DesignEnvironment()
prj = de.new_mws()
m3d = prj.model3d
# --- Units ---
vba_units = '\n'.join([
'With Units',
' .Geometry \"um\"',
' .Frequency \"THz\"',
' .Time \"ns\"',
' .TemperatureUnit \"Kelvin\"',
' .Voltage \"V\"',
' .Current \"A\"',
' .Resistance \"Ohm\"',
' .Conductance \"Siemens\"',
' .Capacitance \"F\"',
' .Inductance \"H\"',
'End With',
])
m3d.add_to_history('define units', vba_units)
# --- Frequency range ---
m3d.add_to_history('define frequency range', 'Solver.FrequencyRange \"<F_MIN>\", \"<F_MAX>\"')
# --- Background ---
vba_bg = '\n'.join([
'With Background',
' .Type \"Normal\"',
' .Epsilon \"1.0\"',
' .Mu \"1.0\"',
' .XminSpace \"0.0\"',
' .XmaxSpace \"0.0\"',
' .YminSpace \"0.0\"',
' .YmaxSpace \"0.0\"',
' .ZminSpace \"0.0\"',
' .ZmaxSpace \"0.0\"',
'End With',
])
m3d.add_to_history('define background', vba_bg)
# Save the empty project with infrastructure
prj.save_as(r'<PROJECT_PATH>')
prj.close()
"
Replace <F_MIN>, <F_MAX>, and <PROJECT_PATH> with actual values.
Based on user requirements and research findings, define all materials needed. Use the patterns from Section A.5.
Common metamaterial material configurations:
| Application | Metal | Dielectric |
|---|---|---|
| THz absorber | Gold (lossy, sigma=4.1e7) | Polyimide (eps=3.5, tand=0.02) |
| IR absorber | Gold (Drude) | SiO2 (eps=2.1, tand=0.001) |
| GHz absorber | Copper (lossy, sigma=5.8e7) | FR-4 (eps=4.3, tand=0.025) |
| All-dielectric | — | Silicon (eps=11.7, Drude for doped) |
| Graphene-based | Graphene (ohmic sheet) | SiO2 or hBN substrate |
Present the material choices to the user and confirm before applying.
Define all parametric variables that the geometry will reference. This must be done before the user creates geometry so they can use parameter names.
vba = "\n".join([
'MakeSureParameterExists "p", "85"', # unit cell period
'MakeSureParameterExists "t_m", "0.2"', # metal thickness
'MakeSureParameterExists "t_sub", "22"', # substrate thickness
'MakeSureParameterExists "w", "8"', # linewidth
# ... add all parameters from research/user spec
])
m3d.add_to_history("define workspace parameters", vba)
Provide the user with the parameter names and their initial values so they can
reference them in geometry construction (e.g., -p/2 to p/2 for a centered
unit cell).
This is the user's creative step. The agent MUST explicitly prompt the user to create their geometry and then STOP and WAIT for the user's response. Do NOT proceed to Step 6 until the user explicitly confirms geometry is done.
Display this prompt to the user:
=============================================
YOUR TURN: Create the Geometry in CST Studio
=============================================
The CST project is ready for you at:
<PROJECT_PATH>
I've set up:
✓ Units (<geometry_unit> / <freq_unit>)
✓ Materials (<list materials defined>)
✓ Parameters (<list key params with values>)
Please open the project in CST Studio and create your
resonator/absorber geometry. Here are some guidelines:
• Component: use "component1"
• Use parametric dimensions — the parameters available are:
<param_name> = <value> <unit>
<param_name> = <value> <unit>
...
Example: Xrange "-p/2" to "p/2"
• Coordinate system: center at origin
• Layer stack (bottom → top):
Ground plane (metal): z = [0, t_m]
Dielectric spacer: z = [t_m, t_m + t_sub]
Patterned resonator: z = [t_m + t_sub, t_m + t_sub + t_m]
• If you need a simple shape (brick, cylinder), I can
create it for you — just describe what you want.
When you're done, tell me:
"geometry is done" or provide the updated .cst file path.
=============================================
After displaying the prompt, STOP and WAIT. Do not execute any further steps until the user responds with one of:
If the user asks the agent to create geometry:
After the user confirms geometry is complete, the agent configures all remaining simulation infrastructure programmatically. Open the project and apply:
python -c "
import cst.interface as csti
prj = csti.DesignEnvironment().open_project(r'<PROJECT_PATH>')
m3d = prj.model3d
# 1. Boundary conditions (unit cell)
vba_bc = '\n'.join([
'With Boundary',
' .Xmin \"unit cell\"',
' .Xmax \"unit cell\"',
' .Ymin \"unit cell\"',
' .Ymax \"unit cell\"',
' .Zmin \"expanded open\"',
' .Zmax \"expanded open\"',
' .Xsymmetry \"none\"',
' .Ysymmetry \"none\"',
' .Zsymmetry \"none\"',
' .ApplyInAllDirections \"False\"',
' .UnitCellFitToBoundingBox \"True\"',
' .UnitCellAngle \"90.0\"',
'End With',
])
m3d.add_to_history('define boundary conditions', vba_bc)
# 2. Floquet ports
vba_fp = '\n'.join([
'With FloquetPort',
' .Reset',
' .SetDialogTheta \"0\"',
' .SetDialogPhi \"0\"',
' .SetSortCode \"+beta/pw\"',
' .SetCustomizedListFlag \"False\"',
' .Port \"Zmin\"',
' .SetNumberOfModesConsidered \"2\"',
' .SetDistanceToReferencePlane \"0\"',
' .SetUseCircularPolarization \"False\"',
' .Port \"Zmax\"',
' .SetNumberOfModesConsidered \"2\"',
' .SetDistanceToReferencePlane \"0\"',
' .SetUseCircularPolarization \"False\"',
'End With',
])
m3d.add_to_history('define Floquet port boundaries', vba_fp)
# 3. Mesh (Tetrahedral for FD, Hex for TD)
# See Section A.10 for full patterns
# 4. Solver (FD or TD based on target)
# See Section A.11 / A.12 for full patterns
# 5. Field monitors (optional, for visualization)
# See Section A.9 for patterns
prj.save()
prj.close()
"
Solver selection rules:
| Target | Solver | Mesh | Reason |
|---|---|---|---|
| Narrowband absorber | FD Solver | Tetrahedral | Better frequency resolution at resonance |
| Broadband absorber | TD Solver | Hexahedral | Faster for wide frequency sweeps |
| Multiband | FD Solver | Tetrahedral | Need precise resolution at each band |
| Angular dependence | FD Solver | Tetrahedral | Floquet ports with theta/phi sweep |
Read back all parameters from the configured project:
python -c "
import cst.interface as csti
prj = csti.DesignEnvironment().open_project(r'<PROJECT_PATH>')
params = prj.model3d.get_parameter_list()
print('=== PARAMETERS ===')
for name, value in params:
print(f' {name} = {value}')
prj.close()
"
Present a structured summary:
CST Project Configuration
=========================
Project: <project_name>
Path: <project_path>
Solver: <FD Solver / TD Solver>
Frequency: <f_min> - <f_max> THz
Units: Geometry: um, Frequency: THz
Materials:
- Gold_lossy (Lossy metal, sigma = 4.1e7 S/m)
- Polyimide (Normal, epsilon = 3.5, tan_d = 0.02)
Boundaries: Unit cell (XY), Expanded open (Z)
Floquet Ports: Zmin + Zmax, 2 modes each
Geometry Parameters:
Name Value Suggested Bounds Tunable?
---- ----- ---------------- --------
p 85.0 um [50, 120] Yes
t_m 0.2 um [0.1, 1.0] Yes
t_sub 22.0 um [10, 50] Yes
w 8.0 um [fixed] No
...
Ask the user to confirm:
Do not proceed until the user explicitly confirms.
This section handles extending a 1x1 unit cell into an NxN supercell where each meta-atom can have independent geometry parameters. This enables spatial multiplexing, gradient designs, and frequency-selective surfaces with varying elements.
Ask the user:
For an NxN array, create per-atom parametric names using the convention
<param>_r<row>c<col>:
# Example: 3x3 array with per-atom resonator length L and width w
N = 3
params = {}
for row in range(N):
for col in range(N):
suffix = f"_r{row}c{col}"
params[f"L{suffix}"] = base_L # each atom's length
params[f"w{suffix}"] = base_w # each atom's width
# Generate VBA
lines = []
for name, value in params.items():
lines.append(f'MakeSureParameterExists "{name}", "{value}"')
vba = "\n".join(lines)
m3d.add_to_history("define supercell parameters", vba)
Copy the base geometry to NxN grid positions using Transform. The base unit cell is at position (0, 0). Copies go to (ip, jp) for each grid position.
Strategy: Copy all shapes in the base component, then rename each copy with position-specific names and update their parametric references.
# For a 3x3 supercell centered at origin:
# Positions: (-p, -p), (0, -p), (p, -p), (-p, 0), (0, 0), (p, 0), ...
N = 3
period = "p" # parametric name for unit cell period
for row in range(N):
for col in range(N):
if row == 0 and col == 0:
continue # skip origin (base cell already there)
dx = f"{col}*{period}" if col > 0 else f"-{abs(col)}*{period}" if col < 0 else "0"
dy = f"{row}*{period}" if row > 0 else f"-{abs(row)}*{period}" if row < 0 else "0"
# Copy all shapes in the base component
vba = "\n".join([
'With Transform',
' .Reset',
f' .Name "component1:resonator"',
f' .Vector "{dx}", "{dy}", "0"',
' .UsePickedPoints "False"',
' .InvertPickedPoints "False"',
' .MultipleObjects "True"',
' .GroupObjects "False"',
' .Repetitions "1"',
' .MultipleSelection "False"',
' .Transform "Shape", "Translate"',
'End With',
])
m3d.add_to_history(f"copy resonator to r{row}c{col}", vba)
# Rename the copy
copy_suffix = f"_1" # CST appends _1 for first copy
vba_rename = f'Solid.Rename "component1:resonator{copy_suffix}", "resonator_r{row}c{col}"'
m3d.add_to_history(f"rename copy r{row}c{col}", vba_rename)
Also rename the original base cell shape:
vba = 'Solid.Rename "component1:resonator", "resonator_r0c0"'
m3d.add_to_history("rename base resonator", vba)
For shared layers (ground plane, substrate) that span the full supercell, resize them rather than copying:
# Ground plane spanning NxN supercell
vba = "\n".join([
'With Brick',
' .Reset',
' .Name "ground_plane"',
' .Component "component1"',
' .Material "Gold_lossy"',
f' .Xrange "-{N}*p/2", "{N}*p/2"',
f' .Yrange "-{N}*p/2", "{N}*p/2"',
' .Zrange "0", "t_m"',
' .Create',
'End With',
])
m3d.add_to_history("define supercell ground plane", vba)
The supercell period is N times the unit cell period:
vba = "\n".join([
'With Boundary',
' .Xmin "unit cell"',
' .Xmax "unit cell"',
' .Ymin "unit cell"',
' .Ymax "unit cell"',
' .Zmin "expanded open"',
' .Zmax "expanded open"',
' .UnitCellFitToBoundingBox "True"',
' .UnitCellAngle "90.0"',
'End With',
])
m3d.add_to_history("define supercell boundary conditions", vba)
Since UnitCellFitToBoundingBox "True", the boundary automatically fits to the
NxN geometry extents. No explicit period override needed.
For supercells, increase the number of Floquet modes to capture higher-order diffraction from the larger period:
# Rule of thumb: modes = 2 * N^2 (minimum), or 18 for good coverage
n_modes = max(2 * N * N, 18)
vba = "\n".join([
'With FloquetPort',
' .Reset',
' .SetDialogTheta "0"',
' .SetDialogPhi "0"',
' .SetSortCode "+beta/pw"',
' .Port "Zmin"',
f' .SetNumberOfModesConsidered "{n_modes}"',
' .SetDistanceToReferencePlane "0"',
' .SetUseCircularPolarization "False"',
' .Port "Zmax"',
f' .SetNumberOfModesConsidered "{n_modes}"',
' .SetDistanceToReferencePlane "0"',
' .SetUseCircularPolarization "False"',
'End With',
])
m3d.add_to_history("define supercell Floquet ports", vba)
For symmetric supercells, reduce the parameter count:
When symmetry is used, fewer unique parameters are needed:
3x3 independent: 9 atoms × P params = 9P parameters
3x3 with C4: 3 unique atoms × P params = 3P parameters
3x3 with mirror: depends on axis, typically 4-5 unique atoms
Based on user confirmation from Step 7:
L_r0c0, L_r1c0) becomes
independently tunable.Write configs/<project_name>.json matching engine/config.py :: TargetConfig:
{
"name": "<descriptive_name>",
"description": "<one-line description>",
"objective_type": "absorber",
"response_type": "broadband | narrowband | multiband",
"target_metric": "Absorptance",
"freq_min_thz": 1.0,
"freq_max_thz": 5.0,
"target_value": 0.90,
"sub_band_width_thz": 0.5,
"w_worst_band": 0.50,
"w_mean_shortfall": 0.30,
"w_global_min": 0.20,
"narrowband_freq_search_min": 0.3,
"narrowband_freq_search_max": 1.0,
"narrowband_absorption_weight": 0.2,
"template_cst": "templates/<project_name>.cst",
"solver_type": "HF Frequency Domain | HF Time Domain",
"solver_timeout_s": 3600.0,
"poll_interval_s": 10.0,
"frequency_range": [0.5, 6.0],
"units": {
"geometry": "um",
"frequency": "THz",
"time": "ns"
},
"materials": [
{"name": "Polyimide", "type": "Normal", "epsilon": 3.5, "tand": 0.02},
{"name": "Gold_lossy", "type": "Lossy metal", "sigma": 4.1e7}
],
"baseline_params": {
"p": 85.0,
"t_m": 0.2,
"t_sub": 22.0,
"w": 8.0
},
"tunable_params": ["p", "t_m", "t_sub"],
"param_bounds": {
"p": [50, 120],
"t_m": [0.1, 1.0],
"t_sub": [10, 50]
},
"working_dir": "working",
"exports_dir": "exports",
"project_root": ""
}
For supercells, baseline_params and tunable_params include per-atom