Empro Python Cookbook

Download as pdf or txt
Download as pdf or txt
You are on page 1of 107
At a glance
Powered by AI
This document provides an overview of using Python scripting capabilities within Keysight EMPro electromagnetic simulation software.

The document is a cookbook that provides examples of how to perform common tasks like geometry creation, simulation setup, and results analysis using the Python scripting interface in EMPro.

Topics covered include geometry creation and manipulation, defining ports and sensors, running simulations, and working with results.

Keysight EMPro

Scripting Cookbook
to Serve Python

Users Guide

Notices
Keysight Technologies, Inc. 2014

Warranty

Restricted Rights Legend

No part of this manual may be


reproduced in any form or by any
means (including electronic storage and
retrieval or translation into a foreign
language) without prior agreement
and written consent from Keysight
Technologies, Inc. as governed by
United States and international
copyright laws.

The material contained in this


document is provided as is, and is
subject to being changed, without
notice, in future editions. Further, to
the maximum extent permitted by
applicable law, Keysight disclaims all
warranties, either express or implied,
with regard to this documentation
and any information contained herein,
including but not limited to the implied
warranties of merchantability and
fitness for a particular purpose.
Keysight shall not be liable for errors
or for incidental or consequential
damages in connection with the
furnishing, use, or performance of
this document or of any information
contained herein. Should Keysight
and the user have a separate written
agreement with warranty terms
covering the material in this document
that conflict with these terms, the
warranty terms in the separate
agreement shall control.

If software is for use in the performance


of a U.S. Government prime contract
or subcontract, Software is delivered
and licensed as Commercial computer
software as defined in DFAR 252.2277014 (June 1995), or as a commercial
item as defined in FAR 2.101(a) or
as Restricted computer software as
defined in FAR 52.227-19 (June 1987)
or any equivalent agency regulation
or contract clause. Use, duplication
or disclosure of Software is subject
to Keysight Technologies standard
commercial license terms, and nonDOD Departments and Agencies of the
U.S. Government will receive no greater
than Restricted Rights as defined in
FAR 52.227-19(c)(1-2) (June 1987).
U.S. Government users will receive no
greater than Limited Rights as defined
in FAR 52.227-14 (June 1987) or DFAR
252.227-7015 (b)(2) (November 1995),
as applicable in any technical data.

Edition
Second edition, December 2014
Keysight Technologies, Inc.
1400 Fountaingrove Pkwy.
Santa Rosa, CA 95403 USA

Technology Licenses
The hardware and/or software described
in this document are furnished under a
license and may be used or copied only
in accordance with the terms of such
license.

Printed copies may not be current.


Check the Knowledge Center for the latest version.
www.keysight.com/find/eesof-empro-python-cookbook
2

Keysight EMPro Scripting Cookbook

Contents
1 Introduction
What this Cookbook is About
Learning Python

Using Python in EMPro User Interface 6


Using the Commandline Python Interpreter 7
Downloading the Recipes as a Library 9
A Guided Tour 9

2 Creating and Manipulating Geometry


Expressions, Vectors and Parameters 16
Creating Wire Bodies 18
Creating Sheet Bodies 20
Recursively Traversing Geometry: Covering all Wire Bodies 21
Creating Extrusions 22
Creating Traces: Meanders

23

Creating Equation Objects: Sheet Spiral 25


Creating Bondwires with Profile Definitions

27

Flat Lists and Filtering: Getting all Bondwires 30


Using Lofting: Tapered Waveguides 32
Sweeping Paths: Thick Wire Coils 35

3 Defining Ports and Sensors


Creating an Internal Port

43

Creating Waveguide Ports (FEM only) 45


Creating Rectangular Sheet Ports (FEM only) 47
User Defined Waveforms (FDTD only) 48
Importing User Defined Waveforms from CSV Files (FDTD only) 51
Adding a Far Zone Sensor 52
Adding a Planar Near Field Sensor 52
Keysight EMPro Scripting Cookbook

4 Creating and Running Simulations


Setting Up the FDTD Grid 55
Creating an FDTD Simulation 58
Creating an FEM Simulation 59
Waiting Until a Simulation Is Completed 60

5 Post-processing and Exporting Simulation Results


An Incomplete Introduction To Datasets 63
Something About Units

68

Getting Simulation Result with getResult 71


Creating XY Graphs 74
Working with CITI Files

75

Exporting to Touchstone Files 76


Exporting Surface Sensor Results

76

Directly Sampling Near Fields (FEM only) 78


Reducing Dataset Dimensions 79
Plotting Far Zone Fields 81
Multi-port Weighting 83
Maximum Gain, Maximum Field Strength 84
Integration over Time 86
Exporting Arbitrary Datasets to CSV Files

87

Exporting Surface Sensor Topology to OBJ file 91

6 Extending EMPro with Add-ons


Hello World!

95

Adding Dialog Boxes: Simple Parameter Sweep 97


Extending Context Menu of Project Tree: Cover Wire Body 100

References

Keysight EMPro Scripting Cookbook

Keysight EMPro Scripting Cookbook

1
Introduction
What this Cookbook is About
Learning Python

Using Python in EMPro User Interface

Using the Commandline Python Interpreter 7


Downloading the Recipes as a Library 9
A Guided Tour 9

What this Cookbook is About


One of the popular features of EMPro is its extensive scripting interface using
the Python programming language. Users can automate many tasks, such as
creating and manipulating geometries, setting up and launching simulations,
processing results, and even extending the user interface with new controls.
The possibilities that the Python interface provides are endless. This cookbook
includes some of the code snippets written over the years in response to
questions from users on how to apply Python scripting in EMPro. Consider them
recipes of how to perform certain tasks. Some of them can be used verbatim,
while others will serve as a starting point for your own scripts.
The recipes are organized by topic in different chapters. Within each chapter,
they are roughly ordered from basic to advanced.
This cookbook is continuously being updated and more recipes are added.
Updated versions will be posted on the Keysight Knowledge Center.
www.keysight.com/find/eesof-empro-python-cookbook

Learning Python
This cookbook is not an introduction into the Python programming language.
There are many resources already available for learning Python programming
5

Introduction

basics, and we list a few below. Well assume you have at least basic knowledge
of the language, such as its basic data structures (tuples, lists, strings,
dictionaries, ...), control flow (if statements, for loops, exception handling, list
comprehensions, ...), how to define functions and classes, how to use modules,
and what the most common modules of the Python Standard Library are (math,
os, time, ...). Whenever we use more exotic constructs, well briefly mention
them, usually referring to online documentation.
If youre just starting out with Python, we welcome you to the club. Youll love
it [38]! Youll find out its a widely used programming language gaining much
popularity, even as a replacement for MATLAB [9]. Especially for you, weve
compiled a list of our favorite online introductions and resources:
The Python Tutorial The place to start. This tutorial is part of the official Python
documentation and it covers all the basics you need to know. If youve been
through this one, you should be well prepared to understand the recipes
in this book, and start writing your own scripts in EMPro. It assumes you
follow the tutorial using an interactive Python interpreter, see Using the
Commandline Python Interpreter on page 7 on how to start one.
http://docs.python.org/2.7/tutorial/
Python 2.7 quick reference Written by John W. Shipman of the New Mexico Tech
Computer Center, this document covers the same grounds as the tutorial,
but in a reference style. Its a good place to look up basic features, without
submerging yourself in the legalese of the official language reference.
http://infohost.nmt.edu/tcc/help/pubs/python/web/
Python Module of the Week A good overview of the Python Standard Library by
Doug Hellmann. It complements the official documentation by being much
more example oriented. If you want to use a module that youve never used
before, this is a good place to start.
http://www.doughellmann.com/PyMOTW/
Python Books A compilation of free online resources, this bundles links to e-books
or their HTML counterpart. Zed Shaws Learn Python The Hard Way and
Miller & Ranums How to Think Like a Computer Scientist are quite popular
ones that should get you started.
http://pythonbooks.revolunet.com/

Using Python in EMPro User Interface


Python code can be executed in the EMPro user interface from the script editor,
Figure 1.1. Click on View > Scripting to open the editor. Create a new script, type
in your code and hit the run button. The scripts are embedded in the project
files so they are available wherever you open the same project. They can also
be exported and imported. See [7] for more information on how to use the script
editor.
Additionally, add-ons can be written that are loaded in the EMPro application
per user. This means you can use them to extend the user interface independent
6

Keysight EMPro Scripting Cookbook

Introduction

Figure 1.1 Python Editor in EMPro User Interface

of the loaded project. More information on how to do this can be found in


Chapter 6, Exporting Surface Sensor Topology to OBJ file.

Using the Commandline Python Interpreter


EMPro also ships a commandline version of the Python interpreter. To use it, it is
however required that the correct environment variables are set to support the
EMPro runtime libraries. The easiest way to do that is to start it up is through the
emproenv or emproenv.bat shell scripts, as shown in Figure 1.2 and Figure 1.3.
c:\keysight\EMPro2012_09\win32_64\bin\emproenv.bat python
/usr/local/EMPro2012_09/linux_x86_64/bin/emproenv python

Keysight EMPro Scripting Cookbook

Introduction

Figure 1.2 Commandline Python Interpreter on Windows

Figure 1.3 Commandline Python Interpreter on Linux

Keysight EMPro Scripting Cookbook

Introduction

Downloading the Recipes as a Library


The recipes included in this cookbook are available as an EMPro Library. You can
drag-and-drop recipes from the library onto the Project Tree to add them to your
project. Then, you can open them in the Scripting editor to edit and run them.
Take the following actions to load the library in EMPro:
1 Download ScriptingCookbookLibrary.zip from the Keysight Knowledge Center.
www.keysight.com/find/eesof-empro-python-cookbook
2 Unarchive the downloaded file.
3 Open the Libraries window by clicking on View > Libraries.
4 In the Libraries window, click on File > Add Library..., select the unarchived
library and click Add.

A Guided Tour
Lets see how you can create a new project from scratch by just using Python
scripting. Start by ... creating a new project:
empro.activeProject.clear()

Creating Geometry
Substrate
The project is going to need some geometry. Read the introduction of Chapter 2
of the cookbook, page 9 and 10. Now, create box of 20 mm by 15 mm and 2 mm
high and add it to the project1 :
model = empro.geometry.Model()
model.recipe.append(empro.geometry.Box("20 mm", "2 mm", "15 mm"))
empro.activeProject.geometry().append(model)

It will need some material to be used in simulations. Materials can be added by


name from the default library. Add FR-4 to the project like so:
empro.activeProject.materials().append(
empro.toolkit.defaultMaterial("FR-4"))

Now set the subtrate material to FR-4. The substrate is the first part in the list,
so you can access it on index 0 (zero) of geometry. Materials can be retrieved
by name. You can type the following on one line, the backslash at the end of the
first line indicates that the following code could not be printed on one line and
that its continued on the next (though you can type it verbatim as well, Python
does recognize the backslash too):
1 The order of the arguments for Box are width (X), height (Z) and depth (Y). So not in the XYZ order as
you would expect. Ooops, an unfortunate historical mistake that cannot be corrected without breaking existing
scripts.

Keysight EMPro Scripting Cookbook

Introduction
empro.activeProject.geometry()[0].material = \
empro.activeProject.materials()["FR-4"]

And give the substrate also name. But before you get too tired of typing
empro.activeProject.geometry() all of the time, assign the geometry object
to a local variable parts that you can use as alias.
parts = empro.activeProject.geometry()
parts[0].name = "Substrate"

Groundplane
Youll need a groundplane as well. Do that by using a Sheet Body. In Chapter 2
of the cookbook, study the sections Creating Wire Bodies and Creating Sheet
Bodies. Afterwards, copy the functions makePolyLine, makePolygon and
sheetFromWireBody into a new script. Add the following code, and execute:
verts = [
("-10 mm", "-7.5 mm"),
("10 mm", "-7.5 mm"),
("10 mm", "7.5 mm"),
("-10 mm", "7.5 mm"),
]
wirebody = makePolygon(verts)
sheet = sheetFromWireBody(wirebody)
sheet.name = "Ground"
parts.append(sheet)

This time, you gave it a name before youve added it to the project, which works
just as well.
Give it a material too. Create an alias for empro.activeProject.materials()
too by assigning it to a local variable mats. The groundplane is the second part
added to the parts list, so it should have index 1 (one). But here, well be a bit
more cunning. Use the index -1 (minus one). Just like you can use negative
indices with Python list, -1 will give you the last item from the parts list. Since
youve just added the groundplane, you know its the last one, so use -1 to refer
to it. That saves you from the trouble of keeping tab of the actual indices:
mats = empro.activeProject.materials()
mats.append( empro.toolkit.defaultMaterial("Cu") )
parts[-1].material = mats["Cu"]

Microstrip
Now add a parameterized microstrip on top. Use a trace for that. But first youll
have to add a parameter to the project:
empro.activeProject.parameters().append("myWidth", "2 mm")

Create a wirebody for the centerline:


verts = [
("-10 mm", "0 mm"),
("10 mm", "0 mm"),
]
wirebody = makePolyLine(verts)

Now you can create a model with the Trace feature:


10

Keysight EMPro Scripting Cookbook

Introduction
model = empro.geometry.Model()
trace = empro.geometry.Trace(wirebody)
trace.width = "myWidth"
model.recipe.append(trace)
model.name = "Microstrip"
parts.append(model)

Mmmh, where is it? Oops, its on the bottom side, as its created in the z = 0
plane.
You need to reposition it. There are various ways of doing sojust like in the
UIbut one easy way is setting its anchor point (assuming its translation is
(0, 0, 0)). Since its the last object in the parts list, you can again use -1 as index:
parts[-1].coordinateSystem.anchorPoint = (0, 0, "2 mm")

NOTE

To alter a parts position, you manipulate its coordinateSystem. You cannot set
its origin directly, but you can specify anchorPoint and translation. And since
origin = anchor point + translation, you set both like so:
part.coordinateSystem.translation = (0, 0, 0)
part.coordinateSystem.anchorPoint = (x, y, z)

And give it a material too:


parts[-1].material = mats["Cu"]

Adding Ports
Lets add ports! Browse to Chapter 3 of the Cookbook.
First, you need to create a new Circuit Component Definition. Create a 75 ohm
1 V voltage source. Examine page 34 of the cookbook. It says you dont need to
add them to the project before you can use them, so dont do that. And skip the
waveform things (were going to do FEM later on)
feedDef = empro.components.Feed("Yet Another Voltage Source")
feedDef.feedType = "Voltage"
feedDef.amplitudeMultiplier = "1 V"
feedDef.impedance.resistance = "75 ohm"

Use this definition to create two feeds, one on each side of the microstrip. This
time, well write a function that will help add a port
def addPort(name, tail, head, definition):
port = empro.components.CircuitComponent()
port.name = name
port.definition = definition
port.tail = tail
port.head = head
empro.activeProject.circuitComponents().append(port)
addPort("P1", ("-10 mm", 0, 0), ("-10 mm", 0, "2 mm"), feedDef)
addPort("P2", ("10 mm", 0, 0), ("10 mm", 0, "2 mm"), feedDef)

Keysight EMPro Scripting Cookbook

11

Introduction

Simulating
Now that you have built the entire design, its time to simulate it. You do that by
manipulating the empro.activeProject.createSimulationData() object. To
save some typing, create an alias, by assigning that object to a local variable:
simSetup = empro.activeProject.createSimulationData()

If the default engine in the user interface is the FDTD engine, youll notice some
errors next to the ports because you havent defined proper waveforms. Thats
OK, since youll do an FEM simulation. So the first thing you should do is to
configure the project to use the FEM simulator:
simSetup.engine = "FemEngine"

The errors should disappear.


The default setup is pretty much ready to go, but for the sake of this guided tour,
well configure a frequency plan. Examine plage 50 of the cookbook.
Since the setup already has plan, you need to clear the list first:
freqPlans = simSetup.femFrequencyPlanList()
freqPlans.clear()

Now you can add a new one:


plan = empro.simulation.FrequencyPlan()
plan.type = "Adaptive"
plan.startFrequency = "minFreq"
plan.stopFrequency = "maxFreq"
plan.samplePointsLimit = 10
freqPlans.append(plan)

We used the parameters minFreq and maxFreq, so you should now set the
parameters to the desired values. See pages 1112 of the cookbook.
params = empro.activeProject.parameters()
params.setFormula("minFreq", "1 GHz")
params.setFormula("maxFreq", "5 GHz")

Maybe make some more changes to the simulation settings, like these:
simSetup.femMatrixSolver.solverType = "MatrixSolverDirect"
simSetup.femMeshSettings.autoConductorMeshing = True

Before you can actually simulate, you must save the project. For this guided
tour, well avoid dealing with OpenAccess libraries, and save the project in
legacy format:
empro.activeProject.saveActiveProjectTo(r"C:\tmp\MyProject.ep")

You can now create and run a simulation. The function createSimulation takes
a boolean parameter. If its True, it creates and queues the simulation. If its
False it only creates the simulation. You almost always want it to be True:
sim = empro.activeProject.createSimulation(True)

The return value is the actual simulation object, and its assigned to a variable
sim for further manipulations.
12

Keysight EMPro Scripting Cookbook

Introduction

OK, now your simulation is running and you have to wait for it to end. But how
do you do that programmatorically? Simple, you use the wait function! You pass
the simulation object youve just created and it will wait for its completion, or
failure.
from empro.toolkit.simulation import wait
empro.gui.activeProjectView().showScriptingWindow()
print "waiting ..."
wait(sim)
print "Done!"

How do you know if the simulation has succeeded? Simple, you check its status:
print sim.status

Post-processing
OK, you have now a completed simulation. How do you inspect the results?
Start with importing a few of the modules from the toolkit that you will need:
from empro.toolkit import portparam, dataset, graphing, citifile

If you have a simulation object like youve created before, you can grab the entire
S matrix, and plot it like this:
S = portparam.getSMatrix(sim)
graphing.showXYGraph(S)

If you dont have the simulation object, but you know the simulation ID, you can
use that as well. For example: portparam.getSMatrix(sim='000001'), or
simply portparam.getSMatrix(sim='1')
S is a matrix which uses the 1-based port numbers as indices. So you can also

plot individual S parameters:


graphing.showXYGraph(S[1, 2])

You can also export the S parameters as a CITI file:


citifile.write("C:\\tmp\\MyProject.cti", S)

You can also get individual results using the getResult function from the
empro.toolkit.dataset module. It takes quite a few parameters, but theres
an easy way to get the desired syntax: look if you can find the result in the
Result Browser, right click on it, and select Copy Python Expression, as shown in
Figure 1.4. Then paste it in your script.
Use this technique to copy the expression for the input impedance of port one
(the simulation number 14 will be different in your case):
z = empro.toolkit.dataset.getResult(sim=14, run=1, object='P1',
result='Impedance')
graphing.showXYGraph(z)

Keysight EMPro Scripting Cookbook

13

Introduction

Figure 1.4 Copying the getResult expression for a result available in the Result Browser

14

Keysight EMPro Scripting Cookbook

Keysight EMPro Scripting Cookbook

2
Creating and Manipulating Geometry
Expressions, Vectors and Parameters 16
Creating Wire Bodies 18
Creating Sheet Bodies 20
Recursively Traversing Geometry: Covering all Wire Bodies 21
Creating Extrusions

22

Creating Traces: Meanders

23

Creating Equation Objects: Sheet Spiral 25


Creating Bondwires with Profile Definitions

27

Flat Lists and Filtering: Getting all Bondwires 30


Using Lofting: Tapered Waveguides 32
Sweeping Paths: Thick Wire Coils 35

Creating new geometry is one of the more popular uses of EMPros scripting
facilities. Going from importing third party layouts or CAD, to creating
complex parametrized parts, this chapter is all about manipulating
empro.activeProject.geometry().
Any distinct object in the projects geometry is called a Part. As shown in
Figure 2.1, different kinds of parts exist such as Model, Sketch, and Assembly.
The last one, Assembly, is a container of other parts and as such the projects
geometry is a tree of parts. In fact, empro.activeProject.geometry() is just
an Assembly too, and will also be called the root part hereafter.
Model is the workhorse of the geometry modeling. It will be used for about any
part youll create, the notable exception being wire bodies for which Sketch is
used. A Model basically is a recipe of features: a flat list of operations that
describe how the model must be build. Extrude, Cover, Box, Loft are all

examples of such features. This way of constructing geometry is called


feature-based modeling (FBM).
In the recipes that follow in this chapter, many Model parts will be created and it
usually follows the following pattern:
1 A new model is created.
15

Creating and Manipulating Geometry

Extrude

Model

+recipe
1

Recipe

Cover

Box
0..*

Part

Sketch

0..*

Assembly

Feature

Loft
Pattern

Transform

Figure 2.1 Geometry Class Diagram

2 Features are added to its recipe.


3 The model is appended to an assembly.
Heres an example that creates a box and adds it to the projects root assembly:
model = empro.geometry.Model()
model.recipe.append(empro.geometry.Box("1 cm", "2 cm", "3 cm"))
empro.activeProject.geometry().append(model)

Expressions, Vectors and Parameters


Expressions
In the user interface of EMPro, almost anywhere where you can enter a quantity
(a length, a frequency, a resistance, ...), it is allowed to enter a full expression
with parameters, operators, functions and units [4]. The same expressions are
found on the scripting side as Expression objects. Thats why you see a lot
of functions accepting or returning expressions instead of floats as you might
have expected. You can construct them from a string (formulas), float, int or
another Expression object:
a
b
c
d

=
=
=
=

empro.core.Expression("2 * 1 cm")
empro.core.Expression(3.14)
empro.core.Expression(42)
b * a / 36

In practice however, you wont be using the Expression constructor much in


your scripts, as strings, floats and integers are usually implicitly converted. But
bear in mind that float and int arguments are always interpreted in reference
units. For example, Box takes three Expression parameters for the width,
height and depth. But in the following code fragment, only the width argument
16

Keysight EMPro Scripting Cookbook

Creating and Manipulating Geometry

is an explicit expression argument. The height argument is a string automatically


interpreted as an expression formula, and the depth argument is a float which
automatically interpreted in meters1 . And yet, this still creates a cube of
1 1 1 cm:
from empro import core, geometry
box = geometry.Box(core.Expression("1 cm"), "1 cm", .01)

NOTE

NOTE

Quantities specified without unitsas float, int, or as str without


unitare always interpreted in reference units, see Table 5.1.

It is important to realize that the mini-language of Expression is


totally separate of your Python scripts: expression parameters are no
Python variables or vice-versa, Python functions cannot be used in
expressions. See below how to work with expression parameters in Python.

Vectors
3D position and directions are often represented by
empro.geometry.Vector3d, which is an (x, y, z) triple of Expression objects.
Again, you can often omit the explicit Vector3d constructor and just pass a
tuple of three values or expressions. As an illustration of this flexibility, the
following snippet constructs a line segment from (1, 2, 3) cm to (4, 5, 6) cm,
passing the former using an explicit Vector3d instance and the latter as a
simple tuple:
from empro import core, geometry
segment = geometry.Line(
geometry.Vector3d(core.Expression("1 cm"), "2 cm", .03),
(core.Expression("4 cm"), "5 cm", .06))

Vectors also support basic operations:


a = empro.geometry.Vector3d(1, 2, 3)
b = empro.geometry.Vector3d(4, 5, 6)
c = a + 2 * b

2D directions and positions are likewise served by Vector2d.

Parameters
To create parameters usable in Expression objects, you must append their
name and formula to the parameters list of the active project, optionally adding
a comment.
1 Theres nothing particular about using a string for the height and a float for the depth. Any of the three
parameters can be specified as expressions, strings or floats. This is just an example

Keysight EMPro Scripting Cookbook

17

Creating and Manipulating Geometry


from empro import activeProject
activeProject.parameters().append("foo", "1 cm + 4 dm")
activeProject.parameters().append("baz", "2 GHz", "This is Baz")

To update a parameter, you set a new formula:


activeProject.parameters().setFormula("foo", "2 cm")

Getting the current formula goes as follows:


print activeProject.parameters().formula("baz")

Whenever you want to evaluate a parameter to a Python float, you simply feed
it into an Expression and convert it to a float:
dt = float(empro.core.Expression("timestep"))

The following code snippet will print all parameters available in the project,
together with their current formula and floating-point value:
for name in empro.activeProject.parameters().names():
formula = empro.activeProject.parameters().formula(name)
value = float(empro.core.Expression(name))
print "%(name)10s = %(formula)-20s = %(value)s" % vars()

NOTE

To illustrate Python variables and expression parameters really have no relationship,


try to add a variable as a parameter and then try to change the variable:
buzz = 1
empro.activeProject.parameters().append("buzz", buzz)
buzz = 2
print buzz, "!=", float(empro.core.Expression("buzz"))

When doing so, the parameter wont change


accordingly, so this will output 2 != 1.0.

Creating Wire Bodies


Most geometrical modeling starts with a wire body. Sometimes theyll exist on
their own as thin wire models of dipole antennas (FDTD only), but usually theyre
needed as the profile of a sheet body, or as the cross section of an extrusion.
What is known as a Wire Body in the user interface, is known as a Sketch in the
Python API. It consists of a number of edges that must be added to the sketch.
Different kind of Edge elements exist such as Line, Arc, LawEdge, ...

Creating a Single Line Segment


Heres a very simple example that creates a single line segment. Start by
importing the geometry module so you dont need to type the empro prefix all of
the time. Then create a new Sketch and give it a name. A single Line edge is
18

Keysight EMPro Scripting Cookbook

Creating and Manipulating Geometry

added between two (x, y, z) triples, commonly known as the tail and head.
Finally, the sketch is added to activeProject.
from empro import geometry
sketch = geometry.Sketch()
sketch.name = "my sketch"
sketch.add(geometry.Line((0, 0, 0), ("1 mm", "2 mm", "3 mm")))
empro.activeProject.geometry().append(sketch)

Creating Polylines and Polygons


Adding adjacent line segments to the sketch will result in a polyline or, when
closed, a polygon. Recipe 2.1 shows two functions that can help with this. Both
take a series of vertices that need to be connected, saving you from needing
to duplicate the shared vertices of adjacent line segments. As an example, a
rectangle is created in the XY plane, centered around the origin.
makePolyline is a straight forward extension of the example above, connecting
all vertices. It takes two optional arguments sketch and name: by passing in
an existing Sketch, you can append to it instead of creating a new one.

Each line segment connects two vertices. An interesting way to extract the tails
and heads of individual line segments is demonstrated on line 12, using a bit of
slicing [10]. Suppose there are n vertices, then there are n 1 line segments.
vertices[:-1] yields all but the last of the vertices, and thus the n 1 required
segment tails. vertices[1:] yields all but the first of the vertices, and thus the
n1 required segment heads. zipping both gives us the n1 required (tail, head)
pairs.
makePolygon cunningly reuses makePolyline by observing that a polygon can
be created as a closed polyline, the first vertex being repeated as the last.
Because a single vertex cannot be concatenated to a list, the single-element
slice vertices[:1] is used instead.

NOTE

None is great to use as default value for function parameters: if no argument for the
parameter is specified, it will have the value None. Because None evaluates to False
in Boolean tests, you can easily check if the argument has a valid non-default value:
if name:
sketch.name = name

None is also often used as a substitute for the real default value. For example, if you
want the default name to be Polyline, you can still use None as default value and use
the following recurring idiom using an or clause:
sketch.name = name or "Polyline"

Unlike you might expect, this does not result in sketch.name to be True or False.
Instead x or y yields either x or y [16]:
# z = x or y
z = x if x else y

If name is an actual string, it will be assigned to sketch.name. But if name is Name,


"Polyline" will be assigned instead.
Keysight EMPro Scripting Cookbook

19

Creating and Manipulating Geometry

This idiom can also be used to replace a argument by an actual default value, if it was
unassigned:
sketch = sketch or geometry.Sketch()

Theres one caveat: using the fact that None evaluates to False means that 0, empty
strings, empty lists, or anything that evaluates to False will be considered as an
invalid argument and be replaced by the default value. This is usually acceptable, but
if you want to avoid that, you should explicitly test if the argument is not None [31]:
# "" evaluates to False will also be replaced by "Polyline"
sketch.name = name or "Polyline"
# "" will be accepted and not replaced
sketch.name = name if name is not None else "Polyline"

Recipe 2.1 PolylineAndPolygon.py


def makePolyLine(vertices, sketch=None, name=None):
"""
- vertices: sequence of (x,y,z) coordinates to be connected.
- sketch [optional]: if given, append polyline to it.
Otherwise a new Sketch will be created.
- name [optional]: name of sketch
"""
from empro import geometry
sketch = sketch or geometry.Sketch()
if name:
sketch.name = name
for tail, head in zip(vertices[:-1], vertices[1:]):
sketch.add(geometry.Line(tail, head))
return sketch
def makePolygon(vertices, sketch=None, name=None):
return makePolyLine(vertices + vertices[:1],
sketch=sketch, name=name)
# --- example --if __name__ == "__main__":
width = 0.20
height = 0.10
vertices = [
(-width/2, -height/2, 0),
(+width/2, -height/2, 0),
(+width/2, +height/2, 0),
(-width/2, +height/2, 0)
]
empro.activeProject.geometry().append(
makePolygon(vertices, name="my rectangle"))

Creating Sheet Bodies


Sheet bodies are very simple to construct as they are basically covered
wire bodies. Recipe 2.2 demonstrates the straight forward function
sheetFromWireBody that accomplishes this task. It takes one argument
wirebody which is the Sketch to be covered. An optional name argument allows
to specify a name. As a prime example of the FBM techniques used in EMPro, it
20

Keysight EMPro Scripting Cookbook

Creating and Manipulating Geometry

creates a new Model with exactly one feature: the Cover. The wirebody is
cloned as ownership will be taken, and you want the original unharmed.
The example reuses makePolygon of Recipe 2.1 to create one wire body with
two rectangles, the second enclosed in the first. This way, you can create sheet
bodies with holes.
Recipe 2.2 SheetFromWireBody.py
def sheetFromWireBody(wirebody, name=None):
'''
Creates a Sheet Body by covering a Wire Body.
A new Model is returned.
wirebody is cloned so the original is unharmed.
'''
from empro import geometry
model = geometry.Model()
model.recipe.append(geometry.Cover(wirebody.clone()))
model.name = name or wirebody.name
return model
# --- example --if __name__ == "__main__":
from PolylineAndPolygon import makePolygon
width1 = 0.20
height1 = 0.10
vertices1 = [
(-width1 / 2, -height1 / 2, 0),
(+width1 / 2, -height1 / 2, 0),
(+width1 / 2, +height1 / 2, 0),
(-width1 / 2, +height1 / 2, 0)
]
wirebody = makePolygon(vertices1)
width2 = 0.15
height2 = 0.05
vertices2 = [
(-width2 / 2, -height2 / 2, 0),
(+width2 / 2, -height2 / 2, 0),
(+width2 / 2, +height2 / 2, 0),
(-width2 / 2, +height2 / 2, 0)
]
wirebody = makePolygon(vertices2, sketch=wirebody) # append
sheet = sheetFromWireBody(wirebody, name="my sheet")
empro.activeProject.geometry().append(sheet)

Recursively Traversing Geometry: Covering all Wire Bodies


Suppose youve imported a PCB and all the traces are imported as wire bodies
so you only have their outlines. Thats a problem because you cannot simulate
them as such. First you must cover the outlines as sheet bodies to get the full
traces.
Recipe 2.3 demonstrates how to replace all wire bodies in the project by
equivalent sheet bodies. coverAllWireBodies is a simple recursive function
that iterates over all parts in an assembly. If it finds another Assembly, it simply
descends into it by calling coverAllWireBodies again with the new assembly
Keysight EMPro Scripting Cookbook

21

Creating and Manipulating Geometry

as argument. If it encounters a Sketch, itll use sheetFromWireBody of


Recipe 2.2 to create an equivalent Sheet Body. Using the index from
enumerate [23], the existing Wire Body is simply replaced.
Recipe 2.3 CoverWireBodies.py
def coverAllWireBodies(assembly=None):
'''
Covers all Wire Bodies found in assembly and it's sub-asssemblies.
if assembly is None, empro.activeProject.geometry() will be used.
'''
from SheetFromWireBody import sheetFromWireBody
assembly = assembly or empro.activeProject.geometry()
for (index, part) in enumerate(assembly):
if isinstance(part, empro.geometry.Assembly):
coverAllWireBodies(part)
elif isinstance(part, empro.geometry.Sketch):
assembly[index] = sheetFromWireBody(part)
# --- example --if __name__ == "__main__":
coverAllWireBodies()

Creating Extrusions
Creating extrusions is much like creating sheet bodies, except that Extrude
needs an additional direction and distance. Recipe 2.4 shows
extrudeFromWireBody, in similar fashion as in Recipe 2.2.

NOTE

When extruding a sketch, its best to construct it entirely in the


XY-plane. So use z
=
0 for all of its vertices. Otherwise you
may get some unexpected results. Compare this to extrude
operations in the UI where the cross section is really a 2D sketch.

Recipe 2.4 ExtrudeFromWireBody.py


def extrudeFromWireBody(wirebody, distance, direction=(0,0,1),
name=None):
'''
Creates an extrusion.
A new Model is returned.
wirebody is cloned so the original is unharmed.
'''
from empro import geometry
model = geometry.Model()
model.recipe.append(geometry.Extrude(wirebody.clone(),
distance,
direction))
model.name = name or wirebody.name
return model
# --- example --if __name__ == "__main__":
from PolylineAndPolygon import makePolygon
width1 = 0.20

22

Keysight EMPro Scripting Cookbook

Creating and Manipulating Geometry


height1 = 0.10
vertices1 = [
(-width1 / 2, -height1 / 2, 0),
(+width1 / 2, -height1 / 2, 0),
(+width1 / 2, +height1 / 2, 0),
(-width1 / 2, +height1 / 2, 0)
]
wirebody = makePolygon(vertices1)
extrude = extrudeFromWireBody(wirebody, name="my extrude")
empro.activeProject.geometry().append(extrude)

Creating Traces: Meanders


Creating traces is also similar to creating sheet bodies. Both start from a
Sketch, and both generate a single flat surface. But in case of a trace, the
Sketch represents the centerline rather than the outline. And if the centerline is
polygonal, you can reuse makePolyline of Creating Polylines and Polygons on
page 19 to create the Sketch.
Recipe 2.5 shows makeMeander that generates a meandering trace in the XYplane, as shown in Figure 2.2, so that the endpoints of the meander are (0, 0, 0)
and (length, 0, 0).
makeMeander starts by sanitizing the arguments by evaluating everything to
floating point values. Whatever the caller specifies as argumenta float, an
int, an Expression object or an expression as str, the function will continue
to work with the floating point representation. Before casting them to a float,
each argument is first converted to an Expression so that strings are correctly

interpreted as expressions. Attempting to directly cast a string to a float will fail


for expressions like "a * b" or "1 cm". For more information, see Parameters
on page 17.
The default value for pitch is twice the traceWidth, but you cant specify that
as such in the function definition. Instead, the default value for pitch is None,
and it is later replaced by twice the traceWidth. See note on page 19 for more
details on this technique.
Next up is calculating how many meanders can fit between start and endminus
the minimal leadsand how long each meander should be to achieve the desired
total length.
To create the actual centerline, a helper function x(k) will assist calculating
the x-coordinates of the vertices. In Python you can nest function inside other
functions as many times you want, and they see the variables of the surrounding
scope.
Each meander consists of two vertices with the same y-coordinate. Depending
on whether this is an even or odd meanderthe modulo operation k % 2 returns
the remainder of dividing k by 2the meander extends in the positive or negative
y -direction. The x-coordinate of the second vertex is equal to the first vertex of
the next meander.
Keysight EMPro Scripting Cookbook

23

Creating and Manipulating Geometry

Figure 2.2 Meandering trace parameters

Finally, makePolyline of Recipe 2.1 is reused to create the centerline, which is


used to create the Trace object.
Recipe 2.5 Meander.py
def makeMeander(length, totalLength, traceWidth, pitch=None,
minimalLeadLength=0, name=None):
'''
Creates a Meandering trace in XY plane, along the X-axis.
- length: distance between end-points of meander along x-axis.
So end-points will be (0,0,0) and (length,0,0)
- totalLength: total length of the trace's centerline (or
electrical length)
- traceWidth: width of the trace to be generated
- pitch: center-to-center distance between meanders. By default
this is the double of traceWidth
- minimalLeadLength:
- name: name of model to be created, by default this is "Meander"
'''
from empro import core, geometry
import math
# pitch can be None, in which case the double trace width is used.
pitch = pitch or (2 * core.Expression(traceWidth))
# clean-up parameters
length = float(core.Expression(length))
totalLength = float(core.Expression(totalLength))
pitch = float(core.Expression(pitch))
minimalLeadLength = float(core.Expression(minimalLeadLength))
# how much space do we actually have for the meander?
availableLength = length - 2 * minimalLeadLength
# how many meanders fit?
if availableLength < pitch:
# not even enough space for one meander
# but do one anyway.
n = 1
else:
n = int(math.floor(availableLength / pitch))
# actual lead length
leadLength = (length - n * pitch) / 2
# how much do we extend from the center line?

24

Keysight EMPro Scripting Cookbook

Creating and Manipulating Geometry

meanderExtent = (totalLength - length) / (2 * n)


# make the actual centerline.
def x(k):
return leadLength + k * pitch
verts = [(0, 0), (x(0), 0)] # starting lead
# n meanders
for k in range(n):
isEven = k % 2 == 0
y = meanderExtent if isEven else -meanderExtent
verts += [(x(k), y), (x(k+1), y)]
verts += [(x(n), 0), (length, 0)] # ending lead
centerLine = makePolyLine(verts)
# finally, make the trace
trace = geometry.Trace(centerLine)
trace.width = traceWidth
model = geometry.Model()
model.name = name or "Meander"
model.recipe.append(trace)
return model
if __name__ == "__main__":
meander = makeMeander(length="1 cm", totalLength="10 cm",
traceWidth=".5 mm", minimalLeadLength=".1 cm")
empro.activeProject.geometry().append(meander)

Creating Equation Objects: Sheet Spiral

Another way to create single surface parts is to use a model with an Equation
feature. This creates surfaces parameterized in u and v . Recipe 2.6 shows a
function that uses this to create a sheet spiral of Figure 2.3 with the following

Figure 2.3 Sheet as spiral antenna, using an Equation

Keysight EMPro Scripting Cookbook

25

Creating and Manipulating Geometry

equation:
x (u, v) = ku cos (2u)
y (u, v) = ku sin (2u)
z (u, v) = v

In these equations, v is in the direction of the width of the strip, so it goes from
width
width
2 to + 2 . u = 1 is a full turn, so that k must be the pitch.
makeSheetSpiral starts on line 5 by sanitizing the arguments that can be

EMPro expressions, and evaluates them as floating point values, just like before.
What follows is the usual tandem of first creating a Model object on line 11, and
adding to it a Equation feature on line line 38. You just need to supply three
strings for the x, y and z functions, and 4 values for minimum and maximum u
and v which can be integers, floating point values, Expression options or just
expression strings.
The tricky bit about Equation is that the equations are evaluated in modeling
units. What does that mean? Well, say that the modeling unit in millimeter. If
minimum and maximum u is 0 mm and 10 mm, then the u will go from 0 to 10
and sin (2u) will go through 10 revolvements. Pretty much as you expect. But
if minimum and maximum u is in meters from 0 m to 10 m, then u will actually
go from 0 to 10000! This is especially surprising if you enter a unitless value for
uMin and uMax like floating point values 0 and 10, because these are interpreted
in the reference unit meter.
The solution to that problem, is to introduce a multiplier c that can scale u and
v back to reference units. And you get that multiplier by asking the size of the
modeling unit in reference units.
The x, y and z equations can only have u and v parametersEMPro parameters
are not supported in these equationsso you need to substitute k and c in the
equations using string formatting. Here, the trick with vars() is used to use the
local variable names in the format string.
Recipe 2.6 SheetSpiral.py
def makeSheetSpiral(numTurns, width, pitch, name=None):
from empro import core, geometry
import math
# Clean up arguments
numTurns = int(core.Expression(numTurns))
width = float(core.Expression(width))
pitch = float(core.Expression(pitch))
# Create a new model
model = geometry.Model()
model.name = name or "Sheet Spiral"
# Ranges of u and v, using sequence unpacking to assign
# two variables at once.
# Using floating point numbers means that values will
# be interpreted in reference units (mete).
uMin, uMax = 0, numTurns
vMin, vMax = -width / 2, width / 2
# In x, y and z equations, u and v will be in modeling units

26

Keysight EMPro Scripting Cookbook

Creating and Manipulating Geometry


#
#
#
#
#
#
#
#
c

This means that if uMin and uMax is 0 and 1 (meter),


and if modeling unit is millimeter, then u will actually
range from 0 to 1000.
This is fine for linear equations (x, y and z are interpreted
in millimeters too), but when using that for a number of
revolvements, we need to compensate by scaling it down.
So c is a multiplier (.001 in case of millimeter) to convert
u and v back to reference units.
= model.unit.toReferenceUnits(1)

k
x
y
z

=
=
=
=

pitch
"%(k)s * u * cos(2 * pi * u * %(c)s)" % vars()
"%(k)s * u * sin(2 * pi * u * %(c)s)" % vars()
"v"

# now you know everything, just add the equation and return.
model.recipe.append(geometry.Equation(x, y, z,
uMin, uMax,
vMin, vMax))
return model
width = "5 cm"
pitch = "5 cm"
numTurns = 3
spiral = makeSheetSpiral(numTurns, width, pitch, name="My Spiral")
empro.activeProject.geometry().append(spiral)

Creating Bondwires with Profile Definitions


EMPro supports a Bondwire feature that easily allows placing multiple bondwires
sharing a single profile definition. While the user interface has a nice dialog to
create the profile definitions, here it is demonstrated how you can do the same
using scripting.
An n-segment profile has n + 1 vertices. The first and the last vertex are the
begin- and endpoint of the individual bondwire, so youre left to define the n 1
vertices in the vertical plane in between [5]. Each vertex needs a pair of offsets
(t, z). Each offset can have a different reference and type (Table 2.1 and
Table 2.2).
addBondVertex has the task of adding individual vertices to the profile. Next to
the BondwireDefinition to be modified, it takes the six arguments required to

define a single vertex. The added bonus of the function is that it will check the
offset type and reference arguments for validity. checkValue compares their
value against the TYPES and REFERENCES tuples and raises a ValueError if they
Table 2.1 Bondwire Definition Offset References
Value

UI name

Description

"Begin"
"Previous"

Begin
Previous

"End"

End

Referenced to the begin point of the bondwire.


Referenced to the previous vertex of the profile, or to the begin point
of the bondwire if this is the first vertex of the profile.
Referenced to the end point of the bondwire and in the opposite
direction (from end to beginning).

Keysight EMPro Scripting Cookbook

27

Creating and Manipulating Geometry


Table 2.2 Bondwire Definition Offset Types
Value

UI name

Description

empro.units.LENGTH
empro.units.SCALAR

Length
Proportional

empro.units.ANGLE

Angular

Absolute length or height.


Proportional to horizontal length of bondwire
instance.
An angular relationship so that z/t = tan .
Can be used for either the horizontal or vertical
offset, but not for both.

dont match. Because checkValue also returns the value, its easy to insert the
check in the assignments.
makeJEDECProfile constructs a standard four-segment JEDEC profile [3]. Only
the , and h1 parameters are used, as h2 is specific to each bondwire instance
and d is implicitly available as the scale for the proportional offsets.

The first vertex to be inserted is the most tricky one. You know its height is
z0 = h1 , but it may be an absolute height or proportional to d. Therefore, the
following convention is used: if h1 is an expression with a length unit, assume
its an absolute height. If it has any other unit class, or if it lacks a unit, assume it
is proportional. The unit class can simply be queried on an Expression, but
since h1 can also be an int, float or string, convert it to an Expression
object first (line 44). More information about unit classes can be found in Unit
Class on page 68.
It is also known that the first segment makes an angle with the horizontal
plane. Since youve already set the vertical offset, use the angular specification
for the horizontal one: t0 = .
The second segment is a horizontal line with length d/8, so insert a vertex
referenced to the previous one, and with a proportional t1 = 12.5% and
z1 = 0.
The final segment has a horizontal length of d/2, so reference the third vertex
from the end with t2 = 50%. Its height should have the angular offset z2 = .

NOTE

When adding a BondwireDefinition to activeProject, a copy is made.


When using the definition of making bondwires, its best to make sure youre
using that copy, and not the original definition youve created. So thats
why on line 68, the definition that was just added is retrieve back.

Recipe 2.7 BondwireDefinition.py


def addBondVertex(bondDef, horOffset, horType, horReference,
vertOffset, vertType, vertReference):
'''
add one vertex to bondwire Definition:
- bondDef: instance of BondwireDefinition to be modified in place
- horOffset, vertOffset: horizontal and vertical offset as an
expression
- horType, verType: how the horizontal or vertical offset must be

28

Keysight EMPro Scripting Cookbook

Creating and Manipulating Geometry


interpreted as an absolute offset in the instantiated bondwire.
One of empro.units.SCALAR (=Proportional), empro.units.LENGTH
(=Length) or empro.units.ANGLE (=Angular)
- horReference, vertReference: the basepoint for the horizontal or
vertical offset. One of "Begin" (=begin point of bondwire),
"Previous" (=previous vertex) or "End" (=end point of bondwire
and offset is in the opposite direction)
'''
from empro import units, geometry
def checkValue(value, name, allowedValues):
if not value in allowedValues:
raise ValueError("%(name)s must be one of "
"%(allowedValues)r. Got %(value)r "
"instead" % vars())
return value
TYPES = (units.SCALAR, units.LENGTH, units.ANGLE)
REFERENCES = ("Begin", "Previous", "End")
vert = geometry.BondwireVertex(horOffset, vertOffset)
vert.tUnitClass = checkValue(horType, "horType", TYPES)
vert.zUnitClass = checkValue(vertType, "vertType", TYPES)
vert.tReference = checkValue(horReference, "horReference",
REFERENCES)
vert.zReference = checkValue(vertReference, "vertReference",
REFERENCES)
bondDef.append(vert)
def makeJEDECProfile(alpha, beta, h1,
name="JEDEC", radius="0.5 mil", numberOfSides=6):
from empro import core, units, geometry
# figure out if h1 should be Proportional or a Length.
h1Type = core.Expression(h1).unitClass()
if h1Type != units.LENGTH:
h1Type = units.SCALAR
bondDef = geometry.BondwireDefinition(name, radius, numberOfSides)
addBondVertex(bondDef,
alpha, units.ANGLE, "Begin",
h1, h1Type, "Begin")
addBondVertex(bondDef,
"12.5 pct", units.SCALAR, "Previous",
"0", units.SCALAR, "Previous")
addBondVertex(bondDef,
"50 pct", units.SCALAR, "End",
beta, units.ANGLE, "End")
return bondDef
# --- example --if __name__ == "__main__":
bondDef = makeJEDECProfile("60 deg", "15 deg", "30 pct",
name="My JEDEC")
# hack: add bonddef to activeProject and get it back
empro.activeProject.bondwireDefinitions().append(bondDef)
bondDef = empro.activeProject.bondwireDefinitions()[-1]
# ok, let's add an instance now
bond = empro.geometry.Model()
bond.recipe.append(empro.geometry.Bondwire((0, 0, ".2 mm"),
("2 mm", 0, 0),
bondDef))
empro.activeProject.geometry().append(bond)

Keysight EMPro Scripting Cookbook

29

Creating and Manipulating Geometry

Flat Lists and Filtering: Getting all Bondwires


Flat Lists
In Recipe 2.2, recursion was used to traverse the part hierarchy. There is an
alternative approach if youre not interested in the exact part node within the
tree: flat part lists.
Any Assemblyand thus also empro.activeProject.geometry()has the
flatList method to request a single list of all parts in the assembly, including
the parts of its sub-assemblies (and their sub-assemblies, all the way down2 ). It
takes exactly one Boolean argument: whether or not to include the
sub-assemblies themselves in the list. You usually want to set it to False.
Load the QFN Package example. It consists of a number of assemblies and one
extrude. First, you simple iterate over the root assembly, and you print the type
and name of each part encountered:
for part in empro.activeProject.geometry():
print type(part), part.name

Youll get the following output:


<type
<type
<type
<type
<type
<type
<type
<type
<type

'empro.libpyempro.geometry.Assembly'> cond
'empro.libpyempro.geometry.Assembly'> cond2
'empro.libpyempro.geometry.Assembly'> diel
'empro.libpyempro.geometry.Assembly'> diel2
'empro.libpyempro.geometry.Assembly'> pcvia1
'empro.libpyempro.geometry.Assembly'> pcvia2
'empro.libpyempro.geometry.Assembly'> pcvia3
'empro.libpyempro.geometry.Assembly'> Bondwire
'empro.libpyempro.geometry.Model'> Board

Repeat the exercise on the flat list, passing True as argument:


for part in empro.activeProject.geometry().flatList(True):
print type(part), part.name

The output youll will look as following. This is only a snippet and the order in
which the parts are printed may varyflatList does not return the parts in the
same order as they appear in the treebut you can already notice parts like bw1
which exist in the Bondwire assembly.
<type
<type
<type
<type
<type
<type
<type
<type
...

'empro.libpyempro.geometry.Assembly'> pcvia3
'empro.libpyempro.geometry.Assembly'> cond2
'empro.libpyempro.geometry.Assembly'> Bondwire
'empro.libpyempro.geometry.Model'>
'empro.libpyempro.geometry.Model'>
'empro.libpyempro.geometry.Model'> Board
'empro.libpyempro.geometry.Model'>
'empro.libpyempro.geometry.Model'> bw1

Try that again, but now pass False as argument:


for part in empro.activeProject.geometry().flatList(False):
print type(part), part.name
2 Or up, depending how you look at it. Were used to draw part trees with the root node at the top and leaf
nodes at the bottom, but the nomenclature suggests otherwise. Well stick to the top-down representation.

30

Keysight EMPro Scripting Cookbook

Creating and Manipulating Geometry

Youll get similar output, but notice that the assemblies are no longer listed. This
is the mode in which youll usually want to use it.
<type
<type
<type
<type
<type
<type
<type
<type
...

'empro.libpyempro.geometry.Model'>
'empro.libpyempro.geometry.Model'>
'empro.libpyempro.geometry.Model'> Board
'empro.libpyempro.geometry.Model'>
'empro.libpyempro.geometry.Model'> bw1
'empro.libpyempro.geometry.Model'>
'empro.libpyempro.geometry.Model'>
'empro.libpyempro.geometry.Model'>

Filtering
Once you have a flat list of all parts, its easy to filter them as well using a list
comprehension [15]. So lets put that knowledge to some practical use.
Recipe 2.8 shows a function that returns all bondwire parts that exist in the
project, no matter how deeply they are buried in the part hierarchy. When simply
executing the script, it will print a list of parts in similar fashion as above, and
you can see it only shows the bondwire parts indeed:
<type
<type
<type
<type

'empro.libpyempro.geometry.Model'>
'empro.libpyempro.geometry.Model'>
'empro.libpyempro.geometry.Model'>
'empro.libpyempro.geometry.Model'>

bw3
bw4
bw2
bw1

The hartand return valueof allBondwires is the list comprehension on line 9.


It filters the flat list based on a predicate isBondwire, which simply accepts one
part and returns True if it is indeed a bondwire. The way this works is pretty
simple. All bondwires are Model parts that have a recipe with a Bondwire as first
feature. isBondwire first grabs the recipe but guards this in a try/except
AttributeError clause because not all parts have recipes (assemblies,
sketches, ...) in which case it simply returns False. Once it has the recipe, it
simply checks if the first feature is a Bondwire.3
With this function, you can for example easily replace the bondwire definition of
all bondwires in the project. Assuming youve created a bondwire definition like
in Creating Bondwires with Profile Definitions on page 27, you can do:
for part in allBondwires():
part.recipe[0].definition = bondDef

There is of course an endless variation of possible predicates that can be used.


One very easy thing to do is filtering on part names. The following gives you a
list of all parts with names starting with bw, which in this case is the same list of
bondwires:
[part for part in empro.activeProject.geometry().flatList(False)
if part.name.startswith("bw")]

Recipe 2.8 AllBondwires.py


def allBondwires(assembly=None):
def isBondwire(part):
3 Instead of the try/except clause to grab the recipe, you can also use isinstance to first check
if the part is a Model. In Python it is however more idiomatic to ask forgiveness rather than permission, or
EAFP [13].

Keysight EMPro Scripting Cookbook

31

Creating and Manipulating Geometry


try:
recipe = part.recipe
except AttributeError:
return False
return isinstance(recipe[0], empro.geometry.Bondwire)
assembly = assembly or empro.activeProject.geometry()
return [part for part in assembly.flatList(False)
if isBondwire(part)]
# --- example --if __name__ == "__main__":
for part in allBondwires():
print type(part), part.name

Using Lofting: Tapered Waveguides


Simple rectangular waveguides are easily created by subtracting two boxes from
each other, one for the outside and one for the inside. But what if you want to
create something more complex, like an exponentially tapered waveguide as in
Figure 2.4? In Recipe 2.9, it is shown how you can use lofting to create a
piecewise linear approximation of such a waveguide.
makeExponentialWaveguide creates a waveguide along the X-axis, from x = 0
to x =length. Therefore, it will create a number of rectangular cross sections

in the YZ-plane and connect them using lofting. This way, you get a piecewise
linear approximation. The width and height of the inside of each cross section
follows an exponential relationship, where s is either the y - or z -coordinate, and
sbegin and send are the begin and end values of width or height:

s (x) = send exp ( (length x))

with

Figure 2.4 Exponentially Tapered Waveguide

32

Keysight EMPro Scripting Cookbook

Creating and Manipulating Geometry

1
send
ln
length sbegin

So each cross section is a rectangular sheet with a rectangular hole. However,


the Loft feature cannot properly handle holes. To work around that, create the
outer and inner rectangles of the cross sections as separate sheet bodies, and
loft them separately. This is taken care of by a helper function frustum. Call
that function twice to get the outer and inner volume of one waveguide section,
and then use a Boolean operation to subtract the former from the latter.
frustum calls makeLoft which helps creating a lofting between two faces in

general. Faces are identified using ID strings, but the ID itself is not enough.
You also need to know which part the face belongs to. So, makeLoft takes as
input two part references and two face ID strings. If a part has only one face,
like sheet bodies, then it is not required to provide the face ID since theres only
one possibility anyway. Thats where fixFace comes in to play. It corrects the
incoming face ID if necessary:
If a face ID is omitted, it will assert the part indeed had only one face and pick
up its ID.
The Loft feature is very particular about face IDs to be used. It are not the
ones from the original faces, but from some internal processed face. Use the
form "face:LP<side>(<face>)" where <side> is either 1 or 2, and <face>
is the original face ID. fixFace will take care of this too.
The Loft feature also stores references to the parts being connected. If you
want to store copies of the original parts, use clone=True. To condense the two
cases in one line, a conditional expression is used [17].
Once all sections are made, they are united in one Boolean operation on line
line 67. The strange way to call uniteMulti is because it demands two
arguments: the blank part and a list of tool parts. In case of a subtraction, this is
logical as you subtract the tools from the blank, but for a union this seems
somewhat odd. Yet, to accommodate this function call, supply the first section
as the blank and all the others as the tools.
Recipe 2.9 LoftingWaveguide.py
def makeExponentialWaveguide(startWidth, startHeight, endWidth,
endHeight, length, thickness,
steps=5, name=None):
'''
Creates and returns exponentially tapered waveguide along X-axis.
- startWidth and startHeight: Y and Z size of inner rectangle at x=0
- endWidth and endHeight: Y and Z size of inner rectangle at
x=length
- length: length of waveguide along X-axis.
- thickness:
- steps: number of sections for approximation [default=5]
- name: name of waveguide [default="waveguide"]
'''
from empro.core import Expression
from empro.geometry import Vector3d, Assembly, Boolean
from empro.toolkit.geometry import plateYZ
import math
# sanitize the arguments

Keysight EMPro Scripting Cookbook

33

Creating and Manipulating Geometry


startWidth = float(Expression(startWidth))
startHeight = float(Expression(startHeight))
endWidth = float(Expression(endWidth))
endHeight = float(Expression(endHeight))
length = float(Expression(length))
thickness = float(Expression(thickness))
steps = int(Expression(steps))
name = name or "waveguide"
def frustum(x0, y0, z0, x1, y1, z1, name):
''' create two rectangular sheets and loft in between '''
sheet0 = plateYZ(Vector3d(x0, 0, 0),
Expression(y0), Expression(z0),
"%s-sheet0" % name)
sheet1 = plateYZ(Vector3d(x1, 0, 0),
Expression(y1), Expression(z1),
"%s-sheet1" % name)
return makeLoft(sheet0, sheet1, name=name)
alphaWidth = -math.log(startWidth / endWidth) / length
alphaHeight = -math.log(startHeight / endHeight) / length
dx = length / steps
tt = 2 * thickness
# create all pieces of the waveguide and gather than in the
# sections list
x0, y0, z0 = 0, startWidth, startHeight
sections = []
for k in range(steps):
x1 = (k + 1) * dx
y1 = endWidth * math.exp(-alphaWidth * (length - x1))
z1 = endHeight * math.exp(-alphaHeight * (length - x1))
# create both volumes ...
inner = frustum(x0, y0, z0,
x1, y1, z1,
"%(name)s-inner-%(k)d" % vars())
outer = frustum(x0, y0 + tt, z0 + tt,
x1, y1 + tt, z1 + tt,
"%(name)s-outer-%(k)d" % vars())
# ... and subtract to get one section
section = Boolean.subtract(outer, inner)
section.name = "%(name)s-section-%(k)d" % vars()
sections.append(section)
x0, y0, z0 = x1, y1, z1 # for next round
# unite all sections
waveguide = Boolean.uniteMulti(sections[0], sections[1:])
waveguide.name = name or "waveguide"
return waveguide
def makeLoft(part1, part2, face1=None, face2=None,
smoothFactor1=0, smoothFactor2=0, clone=False, name=None):
def fixFace(part, face, index):
if not face and len(part.faces()) != 1:
raise ValueError("part%(index)d has more than one face, "
"you must provide a face ID in "
"face%(index)d" % vars())
face = face or part.faces()[0]
prefix = "face:LP%d" % index
if not face.startswith(prefix):
face = "%(prefix)s(%(face)s)" % vars()
return face
face1 = fixFace(part1, face1, 1)
face2 = fixFace(part2, face2, 2)
loft = empro.geometry.Loft(face1, smoothFactor1,
face2, smoothFactor2)

34

Keysight EMPro Scripting Cookbook

Creating and Manipulating Geometry


loft.part1 = part1.clone() if clone else part1
loft.part2 = part2.clone() if clone else part2
model = empro.geometry.Model()
model.name = name or "Loft"
model.recipe.append(loft)
return model
# --- example --if __name__ == "__main__":
waveguide = makeExponentialWaveguide(startWidth="20 mm",
startHeight="10 mm",
endWidth="60 mm",
endHeight="20 mm",
length="40 mm",
thickness="1 mm",
stepds=20)
empro.activeProject.geometry().append(waveguide)

Sweeping Paths: Thick Wire Coils


Suppose you want to create a model of an RFID antenna like in Figure 2.5. If all
you want is a thin wire model, you can construct one using a wire body. But to
get a thick wire model, youll need to sweep a cross section profile along a path.
Recipe 2.10 will show you exactly how.

Rectangular Coils
It starts with a rather lengthy function pathRectangular to create a wirebody
like in Figure 2.5. This will be the path along which the cross section will be
swept. Starting in the origin, it creates a sequence of adjacent edges: the head
of the last becomes the tail of the next. Most of the coil is just a series of
straight edges with round corners.
Since there are two feed lines, at least one of them needs to cross the coil on
another level. So one will need to be raised by bridgeLz. Depending on
whether feedLength is positive or negative, the feeds will be on the out- or
inside of the coil. This will determine whether itll be the first or the last feed:
The left (and first) feed always connects to the outside of the coil. So if
feedLength is positive, then the right feed needs to be raised.
If negative, it should be the first feed, but since that one always starts in the
origin (x = y = z = 0), the rest of the coil is lowered instead.
Once the first feed is created, move to the right over a distance of
feedSeparation + feedOffset. Compute a new head as if there are no
rounded corners, and then use _side_with_corner to insert the straight edge +
the corner. If cornerRadius is zero, then that function will simply insert a
straight line between tail and head, and move on. Otherwise, it will compute an
adjusted endpoint for the straight edge and then insert the rounded corner. In
both cases, it will return the position to be used as the tail for the next side.
Keysight EMPro Scripting Cookbook

35

Creating and Manipulating Geometry

Figure 2.5 Rectangular coil parameters

So it goes on for a number of turns, keeping the pitch distance between the
lines. Finally it creates the second feed.
crossSectionCircular creates a cross section in the XZ plane, nicely centered
around the origin. It uses an sketch with a single Arc edge which is defined by
three points. Its last argument is True to make a full circle.

Notice that the path starts in the origin and profile is centered around it, and that
the path starts orthogonal (Y axis) to the profile (XZ plane).

NOTE

Path sweeping expects the path to start in a point within a profile, for example its
center. Doing otherwise may yield unexpected results. For best results,
start the sweep path in the origin, and center the profile around the same
origin, in a plane orthogonal to the starting direction of the sweep path.

Once both the path and profile are created, its only a matter of constructing a
new model that uses the SweepPath feature to combine them into a thick model
of the coil. The sweep function defined in the recipe helps with that. At the end
of the script, theres an example of how all this fits together.
Recipe 2.10 RectangularCoil.py
def pathRectangular(numTurns=5, Lx=75e-3, Ly=44e-3, pitch=0.3e-3,
bridgeLz=.5e-3, cornerRadius=0.5e-3,
feedLength=-2e-3, feedOffset=3e-3,
feedSeparation=4e-3, name="Coil"):
'''
- crossSection: the cross section of the wire, an object returned by
crossSectionRectangular or crossSectionCircular
- numTurns: number of turns, integer (full turns only)
- Lx: outer spiral length along x-axis (not accounting for wire

36

Keysight EMPro Scripting Cookbook

Creating and Manipulating Geometry


thickness)
- Ly: outer spiral length along y-axis (not accounting for wire
thickness)
- pitch: spacing between turns (not accounting for wire thickness)
- bridgeLz: height of airbridge above coil
- feedLength: feedline length:
If positive, feeds sticks out of coil and length is measured
from outside (not accounting for wire thickness)
If negative, feeds is on inside of coile and length is measured
from inside (not accounting for wire thickness)
- feedOffset: distance from rightmost feed to right most side of
spiral (not accounting for wire thickness)
- feedSeparation: distance between leftmost and rightmost feed
(not accounting for wire thickness)
- name: name of coil, can be None
'''
from empro import core, geometry
# clean up arguments
numTurns = int(core.Expression(numTurns))
Lx = float(core.Expression(Lx))
Ly = float(core.Expression(Ly))
pitch = float(core.Expression(pitch))
bridgeLz = float(core.Expression(bridgeLz))
cornerRadius = float(core.Expression(cornerRadius))
feedLength = float(core.Expression(feedLength))
feedOffset = float(core.Expression(feedOffset))
feedSeparation = float(core.Expression(feedSeparation))
assert int(numTurns) == numTurns, "numTurns must be integer"
assert numTurns > 0
assert Lx > 0
assert Ly > 0
assert pitch > 0
if feedLength < 0:
feedLength -= (numTurns - 1) * pitch
halfLx, halfLy = Lx / 2, Ly / 2
numSectors = 4
path = geometry.Sketch()
# first feed
tail = start = geometry.Vector3d(0, 0, 0)
head = geometry.Vector3d(0, feedLength, 0)
path.add(geometry.Line(tail, head))
tail = head
# first feed is bridge?
if feedLength < 0:
z = -bridgeLz
head = tail + geometry.Vector3d(0, 0, z)
path.add(geometry.Line(tail, head))
tail = head
else:
z = 0

# to first corner
head = geometry.Vector3d(feedOffset + feedSeparation, feedLength, z)
tail = _side_with_corner(path, tail, head, cornerRadius,
numSectors - 1)
# the coil itself.
for k in range(numTurns):
for sector in range(numSectors):
dx = pitch * (k + (1 if sector >= 3 else 0))
dy = pitch * (k + (1 if sector >= 2 else 0))
sx, sy = _SECTORS[sector]
y = sy * (halfLy - dy) + halfLy + feedLength
isLastSide = (k == numTurns - 1) and \
(sector == numSectors - 1)

Keysight EMPro Scripting Cookbook

37

Creating and Manipulating Geometry


if isLastSide:
head = geometry.Vector3d(feedSeparation, y, z)
path.add(geometry.Line(tail, head))
tail = head
else:
x = sx * (halfLx - dx) - halfLx + \
feedOffset + feedSeparation
head = geometry.Vector3d(x, y, z)
tail = _side_with_corner(path, tail, head, cornerRadius,
sector)
# last feed is bridge?
if feedLength > 0:
z = bridgeLz
head = tail + geometry.Vector3d(0, 0, z)
path.add(geometry.Line(tail, head))
tail = head
# last feed
head = geometry.Vector3d(feedSeparation, 0, z)
path.add(geometry.Line(tail, head))
return path
def crossSectionCircular(radius=0.05e-3):
from empro import core, geometry
radius = float(core.Expression(radius))
assert radius > 0
sketch = geometry.Sketch()
fullCircle = True
sketch.add(geometry.Arc(
(radius,0,0),
(0,0,radius),
(-radius,0,0),
fullCircle
))
return sketch
def sweep(crossSection, path, name=None):
from empro import geometry
model = geometry.Model()
if name:
model.name = name
model.recipe.append(geometry.SweepPath(crossSection, path))
return model
# --- implementation --_SECTORS = (1, 1), (-1, 1), (-1, -1), (1, -1)
def _side_with_corner(path, tail, head, cornerRadius, sector):
from empro.geometry import Vector3d, Line, Arc
import math
if not cornerRadius:
path.add(Line(tail, head))
return head
sx, sy = _SECTORS[sector]
f = 1 - 1 / math.sqrt(2)
cornerTail = head - Vector3d(sx * cornerRadius, 0, 0)
cornerHead = head - Vector3d(0, sy * cornerRadius, 0)
if sx == sy:
cornerTail, cornerHead = cornerHead, cornerTail
cornerMid = head - Vector3d(f * sx * cornerRadius,
f * sy * cornerRadius,
0)

38

Keysight EMPro Scripting Cookbook

Creating and Manipulating Geometry


Table 2.3 Approximation Policies for Polygonal Cross Sections
Constant

Description

INSCRIBED

Use an inscribed polygon as approximation of the circle, the radius


remains unmodified. The resulting 3D wire will have a smaller volume
and smaller surface area.
Use a polygon with the same area as the original circle. The resulting 3D
wire will have the correct volume, but a greater surface area.
Use a polygon with the same perimeter as the original circle. The
resulting 3D wire will have the correct surface area, but a smaller
volume.

EQUAL_AREA
EQUAL_PERIMETER

fullCircle = False
path.add(Line(tail, cornerTail))
path.add(Arc(cornerTail, cornerMid, cornerHead, fullCircle))
return cornerHead
# --- example --if __name__ == "__main__":
cross = crossSectionCircular("50 um")
path = pathRectangular(numTurns=5, Lx="60 mm", Ly="40 mm",
pitch="0.3 mm", bridgeLz="0.3 mm",
cornerRadius="5 mm", feedLength="5 mm",
feedOffset="9 mm", feedSeparation="3 mm")
coil = sweep(cross, path, name="RFID")
empro.activeProject.geometry().append(coil)

More Cross Sections


Recipe 2.11 shows two more examples of cross sections. Both are defined in the
XZ-plane, just like before. One is a simple rectangle, the other is a polygonal
approximation of a circle. Apart from the number of sides and the radius of the
circle to be approximated, the polygonal cross section also takes an argument
that controls how the circle must be approximated: preserving the radius, area
or perimeter. Therefore a number of constants is defined on line 16, their exact
meaning is explained in Table 2.3.
Recipe 2.11 MoreCrossSections.py
def crossSectionRectangular(width=0.05e-3, thickness=0.01e-3):
from empro import core, geometry
width = float(core.Expression(width))
thickness = float(core.Expression(thickness))
assert width > 0
assert thickness > 0
x, z = width / 2, thickness/ 2
points = [( x, 0, -z),
( x, 0, z),
(-x, 0, z),
(-x, 0, -z)]
# reuse makePolygon from PolylineAndPolygon.py
return makePolygon(points)
APPROXIMATION_POLICIES = (INSCRIBED,
EQUAL_AREA,

Keysight EMPro Scripting Cookbook

39

Creating and Manipulating Geometry


EQUAL_PERIMETER) = range(3)
def crossSectionPolygonal(radius=0.05e-3, numSides=6,
approximationPolicy=INSCRIBED):
from empro import core, geometry
import math
radius = float(core.Expression(radius))
numSides = int(core.Expression(numSides))
assert radius > 0
assert numSides >= 3
assert approximationPolicy in APPROXIMATION_POLICIES
pie_angle = 2 * math.pi / numSides
if approximationPolicy == EQUAL_AREA:
area = math.pi * (radius * radius)
poly_radius = math.sqrt(2 * area /
(numSides * math.sin(pie_angle)))
elif approximationPolicy == EQUAL_PERIMETER:
perimeter = 2 * math.pi * radius
poly_radius = perimeter / (2 * numSides *
math.sin(pie_angle / 2))
else:
poly_radius = radius
if numSides % 4 == 0:
theta_offset = pie_angle / 2
else:
theta_offset = 0
def vertex(k):
theta = k * pie_angle + theta_offset
return (poly_radius * math.cos(theta),
0,
poly_radius * math.sin(theta))
sketch = geometry.Sketch()
for k in range(numSides):
tail, head = vertex(k), vertex((k + 1) % numSides)
sketch.add(geometry.Line(tail, head))
return sketch

Spiral Coil
Finally, Recipe 2.12 also presents you a function that generates a path for a
circular RFID coil. The function parameters are similar to pathRectangular, but
Lx and Ly are replaced by a single diameter parameter, and feedOffset is no
longer used. A new parameter is discretisationAngle: unless zero, it is used
to approximate the spiral by linear segments, which gives a more predictable
sweeping behavior.
Recipe 2.12 SpiralCoil.py
def pathSpiral(numTurns, diameter, pitch, bridgeLz, feedLength,
feedSeparation, discretisationAngle):
from empro import geometry
import math
# clean up arguments
numTurns = int(core.Expression(numTurns))
diameter = float(core.Expression(diameter))
pitch = float(core.Expression(pitch))
bridgeLz = float(core.Expression(bridgeLz))
feedLength = float(core.Expression(feedLength))
feedSeparation = float(core.Expression(feedSeparation))
discretisationAngle = float(core.Expression(discretisationAngle))
assert int(numTurns) == numTurns, "numTurns must be integer"
assert diameter > 0
assert pitch > 0

40

Keysight EMPro Scripting Cookbook

Creating and Manipulating Geometry

assert feedSeparation >= 0


assert feedSeparation < (diameter - 2 * numTurns * pitch)
if feedLength < 0:
feedLength -= numTurns * pitch
radius = diameter / 2.
center = (-radius, 0, 0)
sketch = Sketch()
# translate seperation to angles, this is an approximation!
rho = lambda theta: (radius - pitch * theta / (2 * math.pi))
halfSeperation = feedSeparation / 2
radiusStart = radius
thetaStart = math.asin(halfSeperation / radiusStart)
radiusEnd = radius - numTurns * pitch
thetaEnd = 2 * math.pi * numTurns - math.asin(halfSeperation /
radiusEnd)
xOffset = -math.sin(thetaStart) * rho(thetaStart)
yOffset = radius + feedLength
# first feed
yStart = -math.cos(thetaStart) * rho(thetaStart) + yOffset
zStart = (0, bridgeLz)[feedLength < 0]
tail = start = (0, 0, 0)
head = (0, yStart, 0)
sketch.add(geometry.Line(tail, head))
tail = head
# first feed is bridge?
if feedLength < 0:
z = -bridgeLz
head = (0, yStart, z)
sketch.add(Line(tail, head))
tail = head
else:
z = 0
# spiral
xy = lambda theta: (math.sin(theta) * rho(theta) + xOffset,
-math.cos(theta) * rho(theta) + yOffset)
xEnd, yEnd = xy(thetaEnd)
if not discretisationAngle:
# use a true spiral wire.
# doesn't really work wel for the sweep though.
center = (xOffset, yOffset, z)
sketch.add(Spiral(center, tail, -pitch,
thetaEnd - thetaStart, Spiral.Right))
tail = (xEnd, yEnd, z)
else:
n = int(math.ceil((thetaEnd - thetaStart) /
discretisationAngle))
for k in range(1, n):
theta = thetaStart + k * discretisationAngle
x, y = xy(theta)
head = (x, y, z)
sketch.add(geometry.Line(tail, head))
tail = head
head = (xEnd, yEnd, z)
sketch.add(geometry.Line(tail, head))
tail = head
# last feed is bridge?
if feedLength > 0:
z = bridgeLz
head = (xEnd, yEnd, z)
sketch.add(geometry.Line(tail, head))
tail = head
# last feed
head = (xEnd, 0, z)

Keysight EMPro Scripting Cookbook

41

Creating and Manipulating Geometry


sketch.add(geometry.Line(tail, head))
tail = head
return sketch

42

Keysight EMPro Scripting Cookbook

Keysight EMPro Scripting Cookbook

3
Defining Ports and Sensors
Creating an Internal Port

43

Creating Waveguide Ports (FEM only) 45


Creating Rectangular Sheet Ports (FEM only) 47
User Defined Waveforms (FDTD only) 48
Importing User Defined Waveforms from CSV Files (FDTD only) 51
Adding a Far Zone Sensor 52
Adding a Planar Near Field Sensor 52

With only geometry, one cannot define a simulation. Excitations need to be


defined to drive the simulation. Sensors need to be added to record simulation
results. In this chapter, it is explored how the design geometry can be
complemented with ports and sensors.

Creating an Internal Port


Recipe 3.1 shows a full example of how to add an internal port. The following
sections will explain it in more detail.

Circuit Components
Internal ports are defined by impedance lines between two endpoints, together
with some properties. To create one, a CircuitComponent with a proper
CircuitComponentDefinition must be inserted in the project. Heres a
minimal example of what to do:
feed = empro.components.CircuitComponent()
feed.name = "My Feed"
feed.definition = empro.activeProject.defaultFeed()
feed.tail = (0, 0, "-1 mm")
feed.head = (0, 0, "+1 mm")
empro.activeProject.circuitComponents().append(feed)

43

Defining Ports and Sensors

This creates a component called My Feed between two points and adds it to
the project.

Circuit Component Definitions


The definition used above is a default one created by EMPro, a 50 voltage
source definition with a proper waveform for FDTD simulations. In real situations,
youll create your own definition though.
All circuit component definitions can be found in the empro.components
module. The most commonly used one is Feed which represents a voltage or
current source. Other types are PassiveLoad, ModalFeed which represents
a power source to compute modal waveguide S parameters, Diode,
NonlinearCapacitor and Switch. The last three are only supported for FDTD,
and ModalFeed is FEM only.
Heres how to define your own voltage source:
feedDef = empro.components.Feed("My Voltage Source")
feedDef.feedType = "Voltage"
feedDef.amplitudeMultiplier = "1 V"
feedDef.impedance.resistance = "50 ohm"
feedDef.waveform = empro.activeProject.defaultWaveform() # for FDTD only
feed.definition = feedDef

The amplitude attribute is called amplitudeMultiplier because for FDTD its


really used to scale a time-domain waveform (which is supposed to be
normalized). The FEM engine doesnt use waveforms, but the attribute is still
called amplitudeMultiplier.

NOTE

In contrary to all other sorts of definitions, its not


required to first add the circuit component definition to
empro.activeProject.circuitComponentDefinitions() before you

can use it (see Creating Bondwires with Profile Definitions on page 27).
You can directly use the created definition for one or more circuit
components, and only add the circuit components to the project. EMPro
will detect what definitions are used, and will automatically add them to
the list of circuit component definitions. The same is valid for waveforms.

Waveforms (FDTD only)


For FDTD simulations, Feed ports need to be driven using a time-domain
waveform. In the previous example, defaultWaveform was used, which
basically generates an Automatic waveform. Creating waveforms manually
consists of two parts: first you create a waveform shape, then you add it to a
waveform object.
Heres how you create a Modulated Gaussian waveform shape, which has two
44

Keysight EMPro Scripting Cookbook

Defining Ports and Sensors

parameters. All shape constructors require a string as name, but theyre not
really used, so you can pass an empty string.
shape = empro.waveform.ModulatedGaussianWaveformShape("")
shape.pulseWidth = "10 ns"
shape.frequency = "1 GHz"

Next, you create a Waveform object. This one does make proper use of the name
string, so call it My Waveform. Then simply set the shape and assign the
complete waveform the the circuit component definition.
waveform = empro.waveform.Waveform("My Waveform")
waveform.shape = shape
feedDef.waveform = waveform

NOTE

Oddly enough, the Automatic and Step waveform shapes are not exported to
Python. The former is exactly whats returned by defaultWaveform, so
you can use that instead. For the latter youll need to fall back to a User
Defined waveform (see User Defined Waveforms (FDTD only) on page 48).

Recipe 3.1 InternalPort.py


import empro
shape = empro.waveform.ModulatedGaussianWaveformShape("")
shape.pulseWidth = "10 ns"
shape.frequency = "1 GHz"
waveform = empro.waveform.Waveform("My Waveform")
waveform.shape = shape
feedDef = empro.components.Feed("My Voltage Source")
feedDef.feedType = "Voltage"
feedDef.amplitudeMultiplier = "1 V"
feedDef.impedance.resistance = "50 ohm"
feedDef.waveform = waveform
feed = empro.components.CircuitComponent()
feed.name = "Feed"
feed.definition = feedDef
feed.port = True
feed.direction = "Automatic" # for FDTD
feed.polarity = "Positive"
feed.tail = (0, 0, "-1 mm")
feed.head = (0, 0, "+1 mm")
empro.activeProject.circuitComponents().append(feed)

Creating Waveguide Ports (FEM only)


Creating waveguide ports from scripting seems a bit tricky at first. You need to
attach the waveguide to a face of a geometrical part, and you need a face ID for
that. Picking tools exist to give this information if you allow for user interaction.
But if you want to do it fully automatically, knowing the right face ID is difficult.
The trick around it is to create auxiliary geometry that has only one face which
ID is known. The prime candidate for this task is a sheet object (or Cover), and
Keysight EMPro Scripting Cookbook

45

Defining Ports and Sensors

thats exactly whats done in Recipe 3.2.


On line 36, a simple rectangular sheet is created. Its size is that of the
waveguide to be created. To make it transparent, set it invisible and exclude it
from the mesh. Mae sure to add it to the project first. Otherwise, you wont have
mesh parameters to be modified. Different than for definitions, adding a
geometry part to the project does not create a copy, so you can keep using the
original one. To get the much needed face ID, simply get the first and only face
of the sheet (line 47).
Ports need definitions, and on line 51, a ModalFeed is created to get modal Sparameters. If you use a Voltage or Current source instead, youll get nodal Sparameters. Adding circuit component definitions to the project create copies,
so to make sure to used the one owned by the project, retrieve the one youve
just appended (line 55)
Finally, prepare a list of (tail, head) pairs for the impedance lines and call
makeWaveguide defined at the top of the script. Its a very straight forward
function. The only weird thing about it is that it sets the grid generator. It
doesnt do anything specific for FEM, but failing to set it may lead to unexpected
results.
Recipe 3.2 WaveguidePort.py
def makeWaveguide(part, faceId, definition, modes=None, name=None):
'''
creates a basic waveguide port
- part: part to attach waveguide to
- faceId: string identifying a face on that part
- definition: a CircuitComponentDefintion from the activeProject.
- modes: optional, a sequence of (tail,head) pairs, defining the
impedance lines for each mode. Instead of a (tail,head) pair,
a sequence item may also be None for a mode without an
impedance line (only for 2D port simulation). When modes is
ommitted, a single mode without impedance line will be
created.
- name: optional, name of the waveguide port.
'''
from empro import components, activeProject
wg = components.WaveGuide(name or "Waveguide Port")
wg.setGridGenerator(activeProject.gridGenerator())
wg.faces = [(part, faceId)]
wg.definition = definition
for m in modes:
mode = components.WaveGuideMode()
if m is not None:
mode.tail, mode.head = m
wg.appendMode(mode)
return wg
# --- example ---if __name__ == "__main__":
Vector3d = empro.geometry.Vector3d
Expression = empro.core.Expression
activeProject = empro.activeProject
# getting a good face Id is the trickiest part.
# Here we'll create a simple sheet, since it only has one face.
sheet = empro.toolkit.geometry.plateXY(Vector3d(0, 0, 0),
Expression("10 mm"),
Expression("20 mm"),
"waveguide geometry")
# Excluding the sheet from the mesh to make it transparent.

46

Keysight EMPro Scripting Cookbook

Defining Ports and Sensors


# first add it to project, otherwise we'll have no meshParameters
activeProject.geometry().append(sheet)
sheet.visible = False
sheet.meshParameters.includeInMesh = False
assert len(sheet.faces()) == 1, \
"assuming that a sheet only has one face!"
faceId = sheet.faces()[0]
# Use a power feed to get modal S-parameters.
# Add copy to project, and retrieve it again.
definition = empro.components.ModalFeed("My Power Feed")
definition.sourcePower = "1 W"
ccDefinitions = activeProject.circuitComponentDefinitions()
ccDefinitions.append(definition)
definition = ccDefinitions[len(ccDefinitions)-1]
impedanceLines = [
(Vector3d("-5 mm", 0, 0), Vector3d("5 mm", 0, 0)),
(Vector3d(0, "-10 mm", 0), Vector3d(0, "10 mm", 0)),
]

# create it and of course, add it to project


waveguide = makeWaveguide(sheet, faceId, definition, impedanceLines,
"waveguide from script")
activeProject.waveGuides().append(waveguide)

Creating Rectangular Sheet Ports (FEM only)


You have imported a design with a lot of internal ports, but they are all defined
as simple impedance lines. You would like to change them into rectangular
sheet ports of a certain width. setRectangularSheetPort of Recipe 3.3 helps
to modify a circuit component to use the rectangular sheet extent. You pass it
the component to be modified and the desired width of the sheet.
To define a sheet port automatically, theres one tricky problem to solve: in what
geometrical plane does the sheet need to be defined? There are an infinite
number of planes passing through the impedance line, and you need to pick the
best one. The best orientation is not uniquely defined however, and therefore
setRectangularSheetPort accepts a third optional argument: the zenith
vector. The sheet extension will be oriented as orthogonal as possible to the
zenith, while still going through the impedance line.
To define a sheet extent, you need to set two corner points of the quadrilateral:
one for the tail and one for the head. The other two corner points are implicitly
defined by mirroring the first two across tail and head. You need an offset vector
orthogonal to both the impedance line and zenith vector, so you take their cross
product. The offsets size should be half the sheets width, so you scale
accordingly.
If the impedance line is parallel to the zenith vector, then their cross product is
the null vector and no proper offset can be determined. When using the default
zenith vector (0, 0, 1), this will happen when applying this recipe to vertical ports.
Using a vertical zenith vector causes the sheet extents to be as horizontal as
possible, and this is impossible to do in case of vertical ports. As a result,
setRectangularSheetPort will fail with an error message, and you should
override the zenith vector with the normal vector of the plane in which you want
Keysight EMPro Scripting Cookbook

47

Defining Ports and Sensors

to define the sheet extent: use (1, 0, 0) for the YZ plane and (0, 1, 0) for the XZ
plane.
Once you have all the information, you assign a new SheetExtent to the
components extent attribute, set its both corner points, and finally you enable
the useExtent flag.
Recipe 3.3 RectangularSheetPort.py
def setRectangularSheetPort(component, width, zenith=(0,0,1)):
'''
Modify a circuit component to use a rectangular sheet port of given
width (half the width on both sides of the impedance line).
The algorithm will attempt to orientate the sheet as orthogonal to
the zenith vector as possible. By default, zenith will be the
z-axis, and the sheet will be orientated as horizontal as possible.
If you want to create vertical sheet ports, you'll have to define a
proper zenith vector yourself. If you want it to be YZ aligned, use
zenith=(1,0,0). If you want it to be XZ aligned, use zenith=(0,1,0).
'''
from empro.core import Expression
from empro.geometry import Vector3d
def cross(a, b):
"cross product of vectors
return Vector3d(a.y * b.z
a.z * b.x
a.x * b.y

a
-

and
a.z
a.x
a.y

b"
* b.y,
* b.z,
* b.x)

width = float(Expression(width))
if not isinstance(zenith, Vector3d):
zenith = Vector3d(*zenith)
tail = component.tail.asVector3d()
head = component.head.asVector3d()
direction = head - tail
offset = cross(direction, zenith)
if offset.magnitude() < 1e-20:
raise ValueError("zenith vector %(zenith)s is parallel to port "
"impedance line, pick one that is orthogonal "
"to it" % vars())
offset *= .5 * width / offset.magnitude() # scale to half width
component.extent = empro.components.SheetExtent()
component.extent.endPoint1Position = tail + offset
component.extent.endPoint2Position = head + offset
component.useExtent = True
# --- example --if __name__ == "__main__":
port = empro.activeProject.circuitComponents()["Port1"]
setRectangularSheetPort(port, "0.46mm", zenith=(1,0,0))

User Defined Waveforms (FDTD only)


So what if none of the available waveform shapes matches what you
want? Then you make your own user defined waveform using
TimestepSampledWaveformShape. The name already discloses what it is: a
waveform shape made from sampled data, and its sampling rate is 1/t where
48

Keysight EMPro Scripting Cookbook

Defining Ports and Sensors

t is the timestep parameter of the FDTD simulation.

Suppose you want to create the following waveform a (t) which looks
suspiciously a lot like the Step Waveform, where Tr is the 10%90% rise time
and t0 is the offset time:

(
(
)2 )

tt0
1 exp 1.423 Tr
t > t0
a (t) =

0
t t0
So you need to evaluate your waveform function in equidistant time samples
0, dt, 2*dt, ..., (n-1)*dt. The sampling rate dt must be the timestep
of the simulation. As described in Parameters on page 17, you can get it by
evaluating the timestep parameter using an Expression object (line 36). The
number of samples n is determined by the minimum of two limits: max_time or
max_samples. If the waveform quickly falls off after an initial pulse, you can set
max_time to generate no more samples than necessary. It is implicitly padded
with zeroes. To avoid generating an extraordinary large amount of samples, you
can also set max_samples. By default, the Maximum Simulation Time of the
Termination Criteria is used to initialized both limits.
The TimestepSampledWaveformShape also requires the derivative of the
waveform. Here, its simply estimated using central differences. A similar
approach as in Creating Polylines and Polygons on page 19 is used to zip As
into a sequence of pairs with the previous and next values. Only the first and the
last value needs to be computed differently.
izip of itertools [24] is used instead of zip to avoid the memory overhead.
zip would create a list of all the pairs, but it only needs to be iterated over once
and then its discarded. This is wastefull. Instead, izip will generate the pairs

on the fly while iterating over them, whithout ever storing the full list in memory.
For the same reason, xrange is used instead of range.
Once you have both the waveform samples and derivatives, the only thing left to
be done is to create the new waveform and give it a timestep sampled shape.
For the example, a step function is created that also takes the rise time and
offset as parameters. To reduce it to a function that only takes a time parameter,
you need to bind the desired rise time to it. There are various ways to do so, but
the most elegant one is using functools.partial available in the Python
Standard Library [37, 25].

NOTE

When using user defined waveforms like this, one must keep the following in mind:
The TimestepSampledWaveformShape is not automatically resampled when the
timestep parameter changes. So for example, when timestep is doubled, the
waveform will be played with half the speed. So you must recreate the waveform,
each time timestep is changed!
When a waveform is added to the project, a copy is made. When using the
waveform, its best to use the copy owned by activeProject. So you add the
waveform to the project, and then you retrieve it back form the project. Quirky, but
necessary.

Keysight EMPro Scripting Cookbook

49

Defining Ports and Sensors

To help with that, Recipe 3.4 also provides a function replace_waveform. The
waveforms list doesnt really act like a Python list or dictionary, and so it needs a bit
of special treatment: you need to look up the index by name, and it will return -1 if it
cant be found instead of raising ValueError. If you want to replace an existing
waveform, you must use the replace method instead a simple assignment. And
waveforms[-1] wont return the last one, so you need to use its length.

Recipe 3.4 UserDefinedWaveform.py


def make_user_defined_waveform(func, max_time=None, max_samples=None,
name=None):
'''
Creates a User Defined Waveform by sampling a function along the
time dimension.
arguments:
- func(t): the function to be sampled.
It's supposed to take one argument: time.
- max_time: limits the time interval in which the function is
sampled to [0, max_time). By default this equals to
terminationCriteria.maximumSimulationTime of
empro.activeProject.createSimulationData()
- max_samples: the maximum number of samples to generate. By
default, this is equals to
terminationCriteria.maximumTimesteps of
empro.activeProject.createSimulationData()
- name: the name of the waveform.
The actual number of samples taken will the the minumum of
max_samples and max_time / dt where dt is the current value of the
expression parameter 'timestep'.
NOTE: if the timestep
created, the waveform
you must execute this
'''
import empro
from itertools import

parameter is changed *after* the waveform is


will not automatically be resampled!
function again to recreate the waveform.
izip

# evaluate arguments
simData = empro.activeProject.createSimulationData()
termCrit = simData.terminationCriteria
max_time = max_time or float(termCrit.maximumSimulationTime)
max_samples = max_samples or termCrit.maximumTimesteps
dt = float(empro.core.Expression("timestep"))
n = min(max_samples, int(max_time / dt))
# sample function
As = [func(k * dt) for k in xrange(n)]
# estimate derivative
dAs = [(As[1] - As[0]) / dt]
dAs.extend((a_p - a_m) / (2 * dt)
for a_m, a_p in izip(As[:-2], As[2:]))
dAs.append((As[-1] - As[-2]) / dt)
assert len(dAs) == n
# create waveform
waveform = empro.waveform.Waveform(name or "")
waveform.shape = empro.waveform.TimestepSampledWaveformShape(As,
dAs)
return waveform
def replace_waveform(waveform):

50

Keysight EMPro Scripting Cookbook

Defining Ports and Sensors


'''
Searches the project for a waveform with the same name.
If it can be found, it's replaced by by a copy of the new one.
Otherwise, the copy of the new waveform is simply appended.
Finally, it returns a reference to the new copy as owned by the
project
'''
import empro
waveforms = empro.activeProject.waveforms()
index = waveforms.index(waveform.name)
if index < 0:
waveforms.append(waveform)
return waveforms[len(waveforms)-1]
else:
waveforms.replace(index, waveform)
return waveforms[index]
def step(t, risetime, offset=0):
import math
if t <= offset:
return 0
return 1 - math.exp(-1.423 * ((t - offset) / risetime) ** 2)
# --- example --if __name__ == "__main__":
import empro
from functools import partial
risetime = float(empro.core.Expression("20 * timestep"))),
waveform = make_user_defined_waveform(partial(step,
risetime=risetime),
name="My Step")
# add to or replace in project, and get waveform _from_ project
waveform = replace_waveform(waveform)
# now, we pick up a circuit component definition from the project
# tree, and we set the waveform + its amplitude
definitions = empro.activeProject.circuitComponentDefinitions()
definitions["50 ohm Voltage Source"].waveform = waveform

Importing User Defined Waveforms from CSV Files (FDTD only)


Reading a waveform from a comma-separated values (CSV) file has
got a whole lot easier in EMPro 2013.07 with the introduction of the
UserDefinedWaveformShapeFromCSVFile waveform shape. You simply create
such a shape, and load the data from the file. Simple. But again, before using
this waveform, you must first add it to the project and get it back from the
project, as explained in User Defined Waveforms (FDTD only) on page 48:
# load waveform
waveform = empro.waveform.Waveform("My Stimulus")
waveform.shape = empro.waveform.UserDefinedWaveformShapeFromCSVFile()
waveform.shape.loadFromFile(r"C:\tmp\stimulus.csv")
# add to project, and get waveform _from_ project
waveforms = empro.activeProject.waveforms()
waveforms.append(waveform)
waveform = waveforms[len(waveforms)-1]

Keysight EMPro Scripting Cookbook

51

Defining Ports and Sensors


# now, we pick up a circuit component definition from the project tree,
# and we set the waveform + its amplitude
definitions = empro.activeProject.circuitComponentDefinitions()
definitions["50 ohm Voltage Source"].waveform = waveform

Adding a Far Zone Sensor


When designing antennas, youll want to setup a far zone sensor in your product
to calculate the antenna gain. This simply requires adding a FarZoneSensor
instance to activeProject.farZoneSensors(). Recipe 3.5 shows how to add
a spherical far zone sensor that collects steady state data. The properties of the
sensor object map directly onto the properties you can find in the user
interface, so this should be straight forward to use. The possible values for
coordinateSystemType are ThetaPhi, AlphaEpsilon and ElevationAzimuth,
which dictates the meaning of the first and second angle properties. When using
a constant angle, only the start value should be set.
Recipe 3.5 FarZoneSensor.py
def makeSphericalFarZoneSensor(resolution="5 deg", name=None):
import empro
sensor = empro.sensor.FarZoneSensor()
sensor.name = name or "Far Zone Sensor"
sensor.coordinateSystemType = "ThetaPhi"
sensor.collectSteadyState = True
sensor.collectTransient = False
sensor.useConstantAngle1 = False
sensor.angle1Start = 0
sensor.angle1Stop = "180 deg"
sensor.angle1Increment = resolution
sensor.useConstantAngle2 = False
sensor.angle2Start = 0
sensor.angle2Stop = "360 deg"
sensor.angle2Increment = resolution
return sensor
# --- example --if __name__ == "__main__":
import empro
fz = makeSphericalFarZoneSensor("10 deg")
empro.activeProject.farZoneSensors().append(fz)

Adding a Planar Near Field Sensor


Adding near field sensors is slightly more complicated than far zone sensors,
as you need to create a separate geometry and data definitionwhich you can
reuse. The following example illustrates how you can add a planar near field
sensor collecting steady state electric field data.
PlaneSurfaceGeometry defines a plane using a position of a point on the
plane, and a normal vector. The properties of SurfaceSensorDataDefinition
map directly onto the properties available in the user interface. You finally create
a SurfaceSensor, set both geometry and definition, and you add it to
52

Keysight EMPro Scripting Cookbook

Defining Ports and Sensors

activeProject.nearFieldSensors(). The definition will automatically be


added to activeProject.sensorDataDefinitions(), its unnecessary to add
it separately, and in fact youre encouraged not to.
plane = empro.sensor.PlaneSurfaceGeometry()
plane.position = ("1 mm", "2 mm", "3 mm")
plane.normal = (0, 0, 1)
definition = empro.sensor.SurfaceSensorDataDefinition()
definition.name = "My Definition"
definition.collectSteadyEFieldVsFOI = True
sensor = empro.sensor.SurfaceSensor()
sensor.name = "My Sensor"
sensor.geometry = plane
sensor.definition = definition
empro.activeProject.nearFieldSensors().append(sensor)

Keysight EMPro Scripting Cookbook

53

54

Defining Ports and Sensors

Keysight EMPro Scripting Cookbook

Keysight EMPro Scripting Cookbook

4
Creating and Running Simulations
Setting Up the FDTD Grid

55

Creating an FDTD Simulation


Creating an FEM Simulation

58
59

Waiting Until a Simulation Is Completed 60

Now you know how to set up geometry, ports and sensors. But how do you
actually simulate your circuit? This chapter is mostly about specifying various
settings, so there are not so many recipes to be found here.

Setting Up the FDTD Grid


All things gridwise start with empro.activeProject.gridGenerator(). There
are many settings to this, but they all correspond to elements in the user
interface. So they wont be explained in detail, but rather their relation to the
settings in the FDTD Grid editor will be shown, tab by tab.

Setting the Grid Size


In the user interface, theres a Basic and Advanced mode for setting the grid
size. In scripting, theres only the advanced mode. Most of the cellSizes
settings take a tuple of three values: for the X, Y and Z directions. Theres the
target and minimum cell sizes to be set, and both accept expressions. So you
can use a tuple of strings, floats, Expression objects, or a mixture of these as
shown in the example below. You can also use Vector3d objects if you
wantsee Vectors on page 17. minimumType corresponds to the Ratio check
box: "RatioType" corresponds with the selected state, "AbsoluteType" makes
the minimum absolute and thus corresponds with the cleared state of the check
box.
grid = empro.activeProject.gridGenerator()
grid.cellSizes.target = ("1.0 mm", 1e-3, "1.0 mm")
grid.cellSizes.minimum = ("0.1 mm", 1e-4, 0.1)
grid.cellSizes.minimumType = ("AbsoluteType", "AbsoluteType",

55

Creating and Running Simulations


"RatioType")

Just to show that using Vector3d also works:


grid.cellSizes.target = Vector3d("1.0 mm", 1e-3, "1.0 mm")

For the padding, you can either set the number of padding cells using the
padding attribute, or you can directly set the bounding box using the
boundingBox attribute. Both have a lower and upper attribute accepting triples
of expressionsas demonstrated in various ways below. You must however be
careful to set the gridSpecificationType to the method youve chosen,
similar to the radio buttons in the user interface:
grid.gridSpecificationType = "PaddingSizeType"
grid.padding.lower = (15, "15", "10 + 5")
grid.padding.upper = (15, 15, 0) # no padding in lower Z direction

or:
grid.gridSpecificationType = "BoundingBoxSizeType"
grid.boundingBox.lower = (-0.015, "-15 mm", "-10 mm - 5 mm")
grid.boundingBox.upper = (0.015, "15 mm", 0)

Adding Fixed Points


Adding a fixed point involves creating a FixedPoint, setting its location and
specifying for which axes it will fix the grid. The axes attribute is a string that
represents a bit field corresponding to the state of the Fixed check boxes. Its
a combination of three flags "X", "Y" and "Z" that can be combined using the
pipe as OR operator. Finally, you add the fixed point to the grid generator.
def addFixedPoint(location, axes="X|Y|Z"):
point = empro.mesh.FixedPoint()
point.location = location
point.axes = axes
empro.activeProject.gridGenerator().addManualFixedPoint(point)
addFixedPoint(("1 mm", "2 mm", 0 ), "X|Y") # for X and Y only.

You can also inspect the currently set fixed points:


for fixed in grid.getManualFixedPoints():
print fixed.location.asVector3d(), fixed.axes

Clearing all of them can be done with grid.clearManualFixedPoints().

Adding Grid Regions


Additional grid regions can be added in similar fashion. You create a
ManualGridRegionParameters and set the desired parameters. cellSizes
works like the global Size settings; gridRegionDirections is a bit field like
axes of a fixed point; and setting regionBounds is a bounding box. Finally, you
add the region to the grid:
region = empro.mesh.ManualGridRegionParameters()
region.cellSizes.target = (".5 mm", ".5 mm", ".5 mm")
region.cellSizes.target = (".5 mm", ".5 mm", ".1 mm")

56

Keysight EMPro Scripting Cookbook

Creating and Running Simulations


region.cellSizes.minimumType = ("AbsoluteType",
"AbsoluteType",
"AbsoluteType")
region.gridRegionDirections = "X|Y|Z"
region.regionBounds.lower = ("-5 mm", "-5 mm", "-5 mm")
region.regionBounds.upper = ("5 mm", "5 mm", 0)
grid.addManualGridRegion(region)

Inspecting and clearing grid regions can be done with getManualGridRegions


and clearManualGridRegions, in similar fashion as for fixed points.

Setting General Limits


In the Limits tab, you can set the Maximum Cell Step Factor and the Maximum
Cells, which correspond to the following properties. The state of the check box is
represented by useMaximumNumberOfCells.
grid = empro.activeProject.gridGenerator()
grid.maximumStepFactor = 2
grid.useMaximumNumberOfCells = True
grid.maximumNumberOfCells = "10 million"

Adding Grid Settings to Individual Objects


Assuming you know what youre doing, you can further refine the grid
settings by having individual objects adding fixed points and grid regions
automaticallybesides the one youve added manually like shown above.
Any part of the projects geometry has a gridParameters attribute that can be
used to further refine the FDTD grid. To have the object to automatically add
fixed points, you first need to set the useFixedPoints attribute. Then specify
fixedPointsLocations and fixedPointsOptions which are again a
combination of string constants that can be ORed. Check the reference
documentation [6] to know what the valid constants are.
Heres a function that will set these parameters for all objects at once. Not all
parts have grid parameters though, in which case part.gridParameters will
evaluate to None and attempting to set any of its attributes will result in an
AttributeError being raised. To prevent the function from failing in such case,
the try / except clause is added to catch and ignore the exception:
def SetAutomaticFixedPointsForAllObjects(locations="All",
options="C1VertexDiscontinuities|GridAxisAlignedLineEndPoints"):
for part in empro.activeProject.geometry().flatList(True):
try:
part.gridParameters.useFixedPoints = True
part.gridParameters.fixedPointsLocations = locations
part.gridParameters.fixedPointsOptions = options
except AttributeError:
pass

By setting useGridRegions you can also have the part to include a grid region
automatically. Specify cellSizes as above. The boundaryExtensions can
be used to expand the region beyond the parts bounding box. Set it in normal
bounding box fashion.
Keysight EMPro Scripting Cookbook

57

Creating and Running Simulations

Creating an FDTD Simulation


The short story to create an FDTD simulation is that you grab the
empro.activeProject.createSimulationData() object to specify the
simulation settings of the simulations to be created, you set its engine to FDTD,
and you create a new simulation with createSimulation.
simSetup = empro.activeProject.createSimulationData()
simSetup.engine = "FdtdEngine"
# ...
sim = empro.activeProject.createSimulation(True)

createSimulation automatically uses the settings specified in


createSimulationData, so theres no parameter to pass it. It does take one

Boolean argument though: whether or not to immediately queue the simulation.


True is like pressing the Create & Queue Simulation button, False is like the
Create Simulation button. The return value of createSimulation is a
Simulation object that you can use to wait for (see Waiting Until a Simulation
Is Completed on page 60), or to retrieve results from (see Chapter 5, Waiting
Until a Simulation Is Completed).

Computing S-parameters
To compute S-parameters, you first need to enable the according option in the
simulation setup:
simSetup.sParametersEnabled = True

You also need to tell what the active feeds are (the rows of your matrix). Below is
a function that accepts a list of port names or numbers and sets the according
ports as active (setting the others as inactive). Its a bit convoluted as
circuitComponents doesnt yet support the iterator protocol, and getting the
port number requires you to call getPortNumber with the index of that port
within circuitComponents. Components that are not a port report a negative
port number.
Recipe 4.1 ActivePorts.py
def setActivePorts(ports):
simSetup = empro.activeProject.createSimulationData()
components = empro.activeProject.circuitComponents()
for k in range(len(components)):
component = components[k]
portNumber = components.getPortNumber(k)
isActive = component.name in ports or \
(portNumber > 0 and portNumber in ports)
simSetup.setPortAsActive(component, isActive)
# --- example --if __name__ == "__main__":
setActivePorts(["Port1", 2])

58

Keysight EMPro Scripting Cookbook

Creating and Running Simulations

Setting Frequencies of Interest for Steady-State Data


The list of steady-state frequencies is managed in the foiParameters attribute.
To collect steady-state data you must first set its collectSteadyStateData
flag. To specify your own frequencies, set foiSource to "Specified".
Setting it to "Waveform" will use the waveform frequency, which only
works for sinusoidal waveforms. Clearing all frequencies can be done
with clearSpecifiedFrequencies and adding new ones with
addSpecifiedFrequency. Heres a function setFrequenciesOfIntereset that
helps specifying a list of frequencies:
def setSteadyStateFrequencies(frequencies):
setup = empro.activeProject.createSimulationData()
foi = setup.foiParameters
foi.collectSteadyStateData = True
foi.foiSource = "Specified"
foi.clearSpecifiedFrequencies()
for f in frequencies:
foi.addSpecifiedFrequency(f)

You can use it with a list of Expression objects, strings or floats:


setSteadyStateFrequencies(["1 GHz",
2e9,
empro.core.Expression("3 GHz")])

You can also do some fancier things with generator expressions [15] to add a
range of frequencies. Heres how to add the series 1 GHz, 2 GHz, ..., 10 GHz.
Just remember that the upper bound of range is excluded from the series1 :
setSteadyStateFrequencies("%s GHz" % k for k in range(1, 11))

Creating an FEM Simulation


Just like for FDTD simulations, the short story is to grab the
createSimulationData object, set the engine to FEM, specify the other
settings and call createSimulation:
simSetup = empro.activeProject.createSimulationData()
simSetup.engine = "FemEngine"
# ...
sim = empro.activeProject.createSimulation(True)

Specifying Frequency Plans


The FEM frequency plans are managed by the femFrequencyPlanList
subobject:
1 Whether or not this makes sense to you, is probably correlated to your preference to either zero- or onebased indexing. There are two major conventions for index ranges: zero-based half-open intervals, and onebased closed intervals. As most general purpose programming languages, Python uses the former as it turns
out to be the practical choice for most applicationsregrettably matrix calculation is not one of them. As a
consequence, the upper bound of range is excluded so that range(len(some_list)) covers all
valid indices for that list, and that len(range(n)) == n. [2, 35]

Keysight EMPro Scripting Cookbook

59

Creating and Running Simulations


freqPlans = simSetup.femFrequencyPlanList()

Individual plans can be added by appending FrequencyPlan objects. Heres


how to add an adaptive frequency plan:
plan = empro.simulation.FrequencyPlan()
plan.type = "Adaptive"
plan.startFrequency = "1 GHz"
plan.stopFrequency = "10 GHz"
plan.samplePointsLimit = 50
freqPlans.append(plan)

Frequency plans of type "Linear" and "Logarithmic" use


numberOfFrequencyPoints instead of samplePointsLimit.
A single frequency can be added as following:
plan = empro.simulation.FrequencyPlan()
plan.type = "Single"
plan.startFrequency = "5 GHz"
freqPlans.append(plan)

Waiting Until a Simulation Is Completed


Now you know how to create and run simulations, but maybe you also want to
add some code to process the results (see Chapter 5, Waiting Until a Simulation
Is Completed). Then youll need to be able to wait until a simulation is
completed. You can write a simple loop that checks the status of the
Simulation object (which is simply the return value of createSimulation)
until its completed. As with any good polling loop, you put in a short sleep. And
in EMPro you also want to put in a processEvents call so that the user interface
stays responsive:
import empro, time
sim = empro.activeProject.createSimulation(True)
while sim.status in ('Queued', 'Running', 'PostProcessing',
'Interrupting', 'Killing'):
time.sleep(.1)
empro.gui.processEvents()

To make things easier, the simulation module in the toolkit has a function wait
that nicely wraps it up for you. You pass it a Simulation object and it will wait
until that simulation is completed.
import empro, empro.toolkit.simulation
sim = empro.activeProject.createSimulation(True)
empro.toolkit.simulation.wait(sim)
print "Done!"

You can also wait for more than one simulation in a single call by passing the
simulation objects in a list:
import empro, empro.toolkit.simulation
sim1 = empro.activeProject.createSimulation(True)
# ...
sim2 = empro.activeProject.createSimulation(True)
empro.toolkit.simulation.wait([sim1, sim2])
print "Both sim1 and sim2 are done!"

60

Keysight EMPro Scripting Cookbook

Creating and Running Simulations

If you call it without any arguments, it will simply wait until all simulations in the
queue are done. The benefit of that is that you dont need to have the simulation
objects, like when creating a sweep:
from empro.toolkit.import simulation
simulation.simulateParameterSweep(width=["4 mm", "5 mm", "6 mm"],
height=["1 mm", "2 mm"])
simulation.wait()
print "All done!"

Keysight EMPro Scripting Cookbook

61

62

Creating and Running Simulations

Keysight EMPro Scripting Cookbook

Keysight EMPro Scripting Cookbook

5
Post-processing and Exporting
Simulation Results
An Incomplete Introduction To Datasets
Something About Units

63

68

Getting Simulation Result with getResult

71

Creating XY Graphs 74
Working with CITI Files

75

Exporting to Touchstone Files

76

Exporting Surface Sensor Results

76

Directly Sampling Near Fields (FEM only) 78


Reducing Dataset Dimensions
Plotting Far Zone Fields

79

81

Multi-port Weighting 83
Maximum Gain, Maximum Field Strength 84
Integration over Time

86

Exporting Arbitrary Datasets to CSV Files

87

Exporting Surface Sensor Topology to OBJ file

91

When you have run a simulation, you want to process its results. In this chapter,
it is explored how you can operate on results to calculate new quantities, how
you can export results, and how you can create graphs within EMPro.
But first, some basics about datasets and units need to covered.

An Incomplete Introduction To Datasets


A DataSet is a scripting type that represents a multidimensional scalar function.
63

Post-processing and Exporting Simulation Results

That sort of wraps it up. Its the alphabut not the omegaof multidimensional
data representation in EMPro. It can be one-dimensional like the time series of
the current through a circuit component. It can also be multidimensional like
far zone data, depending on two angles and the frequency. Most datasets are
discrete; they are sampled for certain values of their dimensions. There are also
continuous datasets, but these are rare.

NOTE

Its important to remember that EMPros DataSet type is


totally unrelated to ADS DataSet files. In this cookbook,
whenever we talk about a dataset, we mean the scripting type.

The following code snippets will assume you have loaded the Microstrip Dipole
Antenna example in EMPro. Dont worry about the getResult calls, well
explain that in Getting Simulation Result with getResult on page 71.

Dataset as a Python Sequence


Discrete datasets are just arrays of floating point numbers. So its only natural to
give them a similar interface like Pythons tuple or list [26]. That means you
can get their size with the len operator, and index values with the [] operators1 :
s11 = empro.toolkit.dataset.getResult(sim=1, run=1, object='Feed',
result='SParameters',
complexPart='ComplexMagnitude')
f = file("s11.txt", "w")
for k in range(len(s11)):
f.write("%s\n" % s11[k])
f.close()

Most datasets are read-only, just like a tuple:


s11[0] = 0.123
# TypeError: 'DataSet' object does not support item assignment

Negative indices work as expected:


if s11[-1] == s11[len(s11) - 1]:
print "OK"

And instead of indexing, you can also iterate over datasets:


out = file("c:/tmp/s11.txt", "w")
for s in s11:
out.write("%s\n" % s)
out.close()

Or even:
out = file("c:/tmp/s11.txt", "w")
out.write("\n".join(map(str, s11)))
out.close()
1 In Python, the len and [] operators are actually called __len__ and __getitem__ [18].
Pronounce them as dunder len and dunder getitem.

64

Keysight EMPro Scripting Cookbook

Post-processing and Exporting Simulation Results

Since datasets support the iterator protocol, you can use many of the functions
that accept sequence arguments:
print min(s11)
print max(s11)
print sum(s11)

You can also use zip to iterate over more than one dataset of the same size, at
the same time:
a11 = empro.toolkit.dataset.getResult(sim=1, run=1, object='Feed',
result='SParameters',
complexPart='Phase')
out = file("c:/tmp/s11.txt", "w")
for s, a in zip(s11, a11):
out.write("%s\t%s\n" % (s, a))
out.close()

Or better, use izip of itertools [24] to avoid the memory overhead of zip:
from itertools import izip
for s, a in izip(s11, a11):
out.write("%s\t%s\n" % (s, a))

The in operator is also supported, but its an O(n) operation, just like for tuple
or list:
if float("nan") in s11:
print "s11 has invalid numbers"

Aspects of the sequence protocol that are not supported are slicing,
concatenationthe + and * operators have other meanings, see Dataset as a
Python Number on page 66and the index and count methods.

Dimensions
All result datasets have one or more dimensions associated with. For example,
an S-parameter dataset will have a dimension that tells the frequency of each
of dataset values. They can be retrieved by index using the dimension method,
and numberOfDimensions will give you their count:
for k in range(s11.numberOfDimensions()):
print s11.dimension(k).name

Theres also the dimensions method that will return a tuple of all dimensions.
So the above could be written more elegantly as:
for dim in s11.dimensions():
print dim.name

Dimensions are DataSets themselves; they support the same methods and
protocols:
freq = s11.dimension(0)
out = file("c:/tmp/s11.txt", "w")
for f, s in zip(freq, s11):
out.write("%s\t%s\n" % (f, s))
out.close()

Keysight EMPro Scripting Cookbook

65

Post-processing and Exporting Simulation Results

Multidimensional Datasets
Dimensions are also used to chunk the single array of values along multiple axes.
For example, the result of a far zone sensor has a frequency and two angular
dimensions, three in total.
fz = empro.toolkit.dataset.getResult(sim=1, object='3D Far Field',
timeDependence='SteadyState',
result='E',
complexPart='ComplexMagnitude')
for dim in fz.dimensions():
print dim.name

The total dataset size is of course the product of the dimension sizes:
n = 1
for dim in fz.dimensions():
n *= len(dim)
if n == len(fz):
print "OK"

To access datasets with an multidimensional index (i, j, k), one cannot use the
[] operator as that indexes the flat array. Instead, you must use the at method
that takes an variable number of arguments:
freq, theta, phi = fz.dimensions()
out = file("c:/tmp/fz.txt", "w")
for i in range(len(freq)):
out.write("# Frequency = %s\n" % freq[i])
for j in range(len(theta)):
for k in range(len(phi)):
out.write("%s\t%s\t%s\n" %
(theta[j], phi[k], fz.at(i, j, k)))
out.close()

The relationship between the flat and multidimensional indices is the following,
where (ni , nj , nk ) are the dimension sizes:
index = ((i nj + j) nk + k) . . .
To verify this is true:
ni, nj, nk = [len(dim) for dim in fz.dimensions()]
ok = True
for i in range(len(freq)):
for j in range(len(theta)):
for k in range(len(phi)):
index = (i * nj + j) * nk + k
if fz[index] != fz.at(i, j, k):
print "oops"
ok = False
if ok:
print "OK"

Dataset as a Python Number


Datasets also behave a lot like normal Python numbers. That means you can
add, subtract, multiply or divide datasets just like they are simple numbers. The
operations are then performed on the individual elements. The requirement for
this to work is that both operands have the same dimensions and are of the
same size.
66

Keysight EMPro Scripting Cookbook

Post-processing and Exporting Simulation Results

Given the port voltage and current time signals, you can compute an
instantaneous impedance suitable for TDR analysis:
v = empro.toolkit.dataset.getResult(sim=1, object='Feed', result='V',
timeDependence='Transient')
i = empro.toolkit.dataset.getResult(sim=1, object='Feed', result='I',
timeDependence='Transient')
z_tdr = v / i

abs will take the absolute value of each dataset value. Combining this with the

sequence protocol, you can compute the root mean square as following:
import math
v_rms = math.sqrt(sum(abs(v) ** 2) / len(v))
print v_rms

Complex Datasets
Since DataSet instances always return scalar values, you need two datasets to
represent complex quantities: one for the real part and one for the imaginary
part. The dataset module in the toolkit contains a class ComplexDataSet that
wraps both and acts like they are one dataset returning complex values.
ComplexDataSet does it very best to walk, swim and quack like a real valued
DataSet [30]. Most of the time, you wont need to bother about this, since
getResult will return a ComplexDataSet automatically when appropriate, and

most of the toolkit functions will understand what to do with it (see Getting
Simulation Result with getResult on page 71). But its good to known that
ComplexDataSet is a wrapper around real valued datasets, rather than a
subclass of the DataSet base class.

Dataset Matrices
The dataset module also contains a DataSetMatrix class, which is useful
when working with many datasets that have the same dimensions, like
S-parameters. It behaves a lot like a regular dict with keys() and values().
Each key is a tuple of two port numbers. To get S12 , you can write:
from empro.toolkit import portparam
s = portparam.getSMatrix(sim=1)
s12 = s[(1, 2)]

And since in Python it is not required to write the tuples parentheses unless
things are ambiguous, you can also write:
s12 = s[1, 2]

Nice.
It also supports a lot of the normal mathematical operators, and a method
inversed() to return the inverse matrix:
y = gRef.inversed() * (s * zRef + e * zRefConj).inversed() * (e - s) * \
gRef

A few more examples are shown further down in this chapter.


Keysight EMPro Scripting Cookbook

67

Post-processing and Exporting Simulation Results

Something About Units


In this chapter, youll see a lot of unit handling code. So its best to have an
introductory section on units.

Unit Class
The term unit class is used to indicate physical quantities like time, length,
electric field strength, ... All datasets have an attribute unitClass that will tell
you want kind of quantity the dataset contains. The value of this attribute is a
string like "TIME", "LENGTH" or "ELECTRIC_FIELD_STRENGTH". Unitless data is
specified using the "SCALAR" unit class.
The full list of known unit classes is available in the documentation, or can be
printed as follows:
for unitClass in sorted(empro.units.displayUnits()):
print unitClass

For your convenience, all these strings are also defined as constants in the
units module: empro.units.TIME, empro.units.LENGTH,
empro.units.ELECTRIC_FIELD_STRENGTH, empro.units.SCALAR, ...

Reference Units
For each unit class, theres an assumed reference unit that is used as an
absolute standard when converting physical quantities from one unit to another.
These reference units simply are the SI units, expanded with directly derived
2
kg
ones like = ms2 A
. The reference unit for plane and solid angles are radians and
steradians. See Table 5.1 for the full list of reference units.

Unit Objects
A unit object is an instance of empro.units.Unit and offers the required
information and functionality for unit to be used in EMPro. It has methods to
query its name, unitClass, preferred abbreviation, or to get a list of
allAbbreviations that are recognized in expressions. To convert from
reference units, it also has a conversionMultiplier, conversionOffset and a
method to know if its a logScale.
To help with the conversion from and to reference units, theres also
fromReferenceUnits and toReferenceUnits that accepts a float argument
and return the converted value as a float.
All known unit objects can be retrieved by abbreviation or by name with
unitByAbbreviation and unitByName. Heres an example that shows the
68

Keysight EMPro Scripting Cookbook

Post-processing and Exporting Simulation Results

Table 5.1 Reference Units


Unit Class

Reference Unit

empro.units.ACCELERATION
empro.units.AMOUNT_OF_SUBSTANCE
empro.units.ANGLE
empro.units.ANGULAR_MOMENTUM
empro.units.ANGULAR_VELOCITY
empro.units.AREA
empro.units.AREA_POWER_DENSITY
empro.units.DATA_AMOUNT
empro.units.DENSITY
empro.units.ELECTRIC_CAPACITANCE
empro.units.ELECTRIC_CHARGE
empro.units.ELECTRIC_CHARGE_DENSITY
empro.units.ELECTRIC_CONDUCTANCE
empro.units.ELECTRIC_CONDUCTIVITY
empro.units.ELECTRIC_CURRENT
empro.units.ELECTRIC_CURRENT_DENSITY
empro.units.ELECTRIC_FIELD_STRENGTH
empro.units.ELECTRIC_POTENTIAL
empro.units.ELECTRIC_RESISTANCE
empro.units.ENERGY
empro.units.FORCE
empro.units.FREQUENCY
empro.units.HEAT_CAPACITY
empro.units.INDUCTANCE
empro.units.LENGTH
empro.units.LUMINOUS_FLUX
empro.units.LUMINOUS_INTENSITY
empro.units.MAGNETIC_FIELD_STRENGTH
empro.units.MAGNETIC_FLUX
empro.units.MAGNETIC_FLUX_DENSITY
empro.units.MASS
empro.units.MASS_POWER_DENSITY
empro.units.MOMENTUM
empro.units.PERFUSION_OF_BLOOD
empro.units.PERMEABILITY
empro.units.PERMITTIVITY
empro.units.POWER
empro.units.PRESSURE
empro.units.SCALAR
empro.units.SOLID_ANGLE
empro.units.THERMAL_CONDUCTIVITY
empro.units.THERMODYNAMIC_TEMPERATURE
empro.units.TIME
empro.units.VELOCITY
empro.units.VOLUME
empro.units.VOLUMETRIC_ENERGY_DENSITY
empro.units.VOLUMETRIC_POWER_DENSITY

m/s**2
mol
rad
N*m*s
rad/s
m**2
W/m**2
bytes
kg/m**3
F
C
C/m**3
S
S/m
A
A/m**2
V/m
V
ohm
J
N
Hz
J/kg/K
H
m
lm
cd
A/m
Wb
T
kg
W/kg
N*s
L/g/s
H/m
F/m
W
Pa

Keysight EMPro Scripting Cookbook

sr
W/m/K
K
s
m/s
m**3
J/m**3
W/m**3

69

Post-processing and Exporting Simulation Results

properties of the micrometers unit. The comments are the expected output of
each print statement:
unit = empro.units.unitByAbbreviation("um")
print unit.name() # micrometers
print unit.abbreviation() # um
print unit.allAbbreviations() # (u'um', u'micron')
print unit.conversionMultiplier() # 1000000.0
print unit.conversionOffset() # 0.0
print unit.logScale() # No_Log_Scale
print unit.fromReferenceUnits(1.2345) # 1234500.0
print unit.toReferenceUnits(1.2345) # 1.2345e-06

Display Units
Each project has a list of units thats normally used to display values. Thats the
list of units thats normally found under Edit > Project Properties... On the scripting
side, that list is represented by the empro.units.displayUnits() dictionary.
You simply index it with a unit class to get the appropriate display unit:
freqUnit = empro.units.displayUnits()[empro.units.FREQUENCY]
print freqUnit.abbreviation() # GHz
print freqUnit.fromReferenceUnits(1e9) # 1.0

In this chapter, youll see the display units used a lot to export data to files.

Backend Units
Internally, physical values are stored and processed in backend units. Youll
encounter these units in two situations:
1 Values retrieved from a DataSet are internal values returned as float, in
backend units.
2 When converting an expression to a float, the result is in backend units:
print float(empro.core.Expression("1 mil")) # will print 2.54e-05

In all normal circumstances, the backend units are exactly the same as the
reference units, see ??. You can verify this by iterating of all of them and
checking that the conversion multiplier is one, the offset is zero, and that this is
not a logarithmic scale:
print "%25s %15s %10s" % ("unit class", "abbreviation", "reference")
for unitClass, unit in sorted(empro.units.backendUnits().items()):
isReference = (unit.conversionMultiplier() == 1 and
unit.conversionOffset() == 0 and
unit.logScale() == "No_Log_Scale")
print "%25s %15s %10s" % (unitClass, unit.abbreviation(),
isReference)

NOTE

When creating your own DataSet objects, its best to populate them with data in
backend units as well. This will avoid weird behavior when exporting or displaying
that data.
The same is true when operating on existing datasets. Do not multiply by 180
to

convert angular data from radians to degrees, but rather use the appropriate

70

Keysight EMPro Scripting Cookbook

Post-processing and Exporting Simulation Results

unit when exporting or displaying. The conversion will be done for you.

Helper Functions
Here are a few helper functions youll see a lot in the following scripts. Theyre
not rocket science, but they help making the scripts a bit more readable.
When writing data to a file, you want to appropriately label that column using
the data name, but also with the abbreviation of the unit in which the data is
displayed. columnHeader helps with this simple task:
def columnHeader(name, displayUnit):
"""
Build a suitable column header using data name and unit.
"""
if unit.abbreviation():
return "%s[%s]" % (name, unit.abbreviation())
else:
return name

Data usually needs to written to files a strings, and you also want to convert that
data from reference to display units. strUnit is a small helper function that
does both at once:
def strUnit(value, displayUnit):
"""
Converts a float in reference units to a string in display units.
"""
return str(displayUnit.fromReferenceUnits(value))

Getting Simulation Result with getResult


To postprocess results, you first need to get them. getResult is your first tool of
the trade here. You probably want to read this section a few times to make sure
you fully understand this.
Fundamentally, retrieving data from simulations requires painstakingly filling in
11 attributes of a ResultQuery, in order! Only then you can retrieve the dataset.
You also should not forget to add the project to the result browser first,
otherwise no data will be available.
For example, to get the current through Port1 of the first run of the FDTD
simulation of the Microstrip Low Pass Filter example, you would need to do all
of this:
empro.output.resultBrowser().addProject(
"C:/keysight/EMPro2012_09/examples/ExampleProjects/"\
"%Microstrip#20%Low#20%Pass#20%Filter")
query = empro.output.ResultQuery()
query.projectId = "C:/keysight/EMPro2012_09/examples/ExampleProjects/" \
"%Microstrip#20%Low#20%Pass#20%Filter"
query.simulationId = '000001'
query.runId = 'Run0001'

Keysight EMPro Scripting Cookbook

71

Post-processing and Exporting Simulation Results


query.outputObjectId = ('CircuitComponent', 'Port1')
query.timeDependence = 'Transient'
query.resultType = 'I'
query.fieldScatter = 'NoFieldScatter'
query.resultComponent = 'Scalar'
query.dataTransform = 'NoTransform'
query.complexPart = 'NotComplex'
query.surfaceInterpolationResolution = 'NoInterpolation'
data = empro.output.ResultDataSet("Current", query)

Scary, isnt it? Luckily, the dataset module in the toolkit contains a function
called getResult that greatly simplifies this:
from empro.toolkit import dataset
current = dataset.getResult(
"C:/keysight/EMPro2012_09/examples/ExampleProjects/" \
"%Microstrip#20%Low#20%Pass#20%Filter",
sim=1, run=1, object='Port1', result='I')

NOTE

In the user interface, for any result listed in the Results Browser, you can retrieve
the corresponding getResult call by clicking Copy Python Expression
in the context menu. Then you simply paste the expression in your script.

getResult works by having a lot of optional parameters and trying to guess

sensible defaults for any parameter not specified. Heres its signature:
def getResult(context=None, sim=None, run=None, object=None,
result=None, timeDependence=None, fieldScatter=None,
component=None, transform=None, complexPart=None,
interpolation=None):

You see almost a one-on-one relationship with the query fields. Thats because
getResult will exactly build such a query for you! Only projectId is missing,
but thats handled by context.
getResult starts with a context in which it needs to interpret the arguments

that follow. It can be many things:


The project path as a string, as in the example above. This tells getResult
to add the project to the result browser if necessary, and to use that string as
the projectId.
A Simulation object retrieved from createSimulation. This not only tells
the projectId, but also already fills in the simulationId. So, when using
this as a context, its no longer necessary to specify the sim parameter2 .
from empro.toolkit import simulation, dataset
sim = empro.activeProject.createSimulation(True)
simulation.wait(sim)
current = dataset.getResult(sim, run=1, object='Port1', result='I')

None. when omitted, the active project is assumed as the context, and the
projectId is set to its rootDir.
Once you have to context, you still need to specify exactly what result you want.
The various parameters you can specify are:
2 Using a Simulation object as context does not work in EMPro 2012.09 for OpenAccess projects.
This is fixed in EMPro 2013.07.

72

Keysight EMPro Scripting Cookbook

Post-processing and Exporting Simulation Results

sim of course lets you set the simulation you want to retrieve data from. It
should either be a full string ID like '000001' or simply its integer equivalent.
If theres only one simulation in the project, you can omit this parameter.
Similarly, run should either be a full run ID like 'Run0001' or an
integerwhich is often the active port number. Again, if theres only one run
in the simulation, you can omit it.
The object parameter needs to be set to the name of the sensor or port you
want to retrieve data from. This is one of the arguments that always needs to
be specified.
result is also one of the parameters that always should be set, and common
values are 'V', 'I', 'E' or 'SParameter'.
timeDependence lets you choose between time and frequency domain, and
it defaults to 'Transient' or 'Broadband', depending on the simulator. You
most likely only need to specify it as 'SteadyState' if you want the discrete
frequencies instead. 'Transient' is FDTD only and 'Broadband' is FEM
only. If you want broadband S-parameters from an FDTD simulation, you need
to get the 'Transient' data and request an FFT transform (see below).
fieldScatter is only interesting for FDTD near fields and defaults to
'TotalField'.
Depending on the result type, component defaults to either 'Scalar' for
scalar data (real or complex) like port parameters, 'VectorMagnitude' for
vector data like near and far fields. Set it to 'X', 'Y' or 'Z' to get individual
3D vector components. Some of the many possible components for far zones
are 'Theta' and 'Phi'.
dataTransform usually defaults to 'NoTransform' and can be set to 'Fft'
to transform the transient FDTD data to frequency domain. Thats also the
default for result types that you normally expect in frequency domain like Sparameters. Its very rare that you need to worry about this option.
When requesting complex-valued data, complexPart allows you to specify if
you want 'RealPart', 'ImaginaryPart', 'ComplexMagnitude' or
'Phase'. These options will all return a real-valued DataSet. In addition,
getResult also supports the 'Complex' option to get both the real and
imaginary parts wrapped as one ComplexDataSet. And thats also
the default. So in most cases, you dont need to worry about this
parameter: real-valued result types will return a real-valued DataSet, and
complex-valued result types will return a ComplexDataSet. Too easy.
interpolation is also one you can mostly ignore and only matters for near
field data.

NOTE

Thats a lot of parameters and a lot of possible arguments, but what you need to
remember is:
The defaults are often what you want anyway.
If you give a wrong option, it will complain with suggestions of what you should
use instead.

Keysight EMPro Scripting Cookbook

73

Post-processing and Exporting Simulation Results

You can get good templates of getResult calls by using the Copy Python
Expression.

Creating XY Graphs
OK, so youve computed some data, but how do you plot it on the screen? The
toolkit contains a module called graphing that can assist with that. Showing an
XY graph is very easy with showXYGraph:
from empro.toolkit import graphing, dataset
v = dataset.getResult(sim=1, run=1, object='Port1', result='V'),
graphing.showXYGraph(v)

The full signature has a number of keyword parameters, which are explained
below:
def showXYGraph(data, ..., title=None, names=None,
xUnit=None, yUnit=None, xBounds=None, yBounds=None,
xLabel=None, yLabel=None):

Specifying Axis Units


By default, the graph axes use display units according to the unit class of the
dataset (for the Y axis) and its dimension (for the X axis). If you want to override
that, you can specify the units abbreviation as following:
graphing.showXYGraph(v, xUnit="ps")

Alternatively, you can also pass a unit object:


dBV = empro.units.unitByAbbreviation("dBV")
print dBV
graphing.showXYGraph(v, yUnit=dBV)

S-parameters are a bit special because as ratios, their unit class is "SCALAR"
and scalar values are shown linearly by default. However, if showXYGraph can
guess youre actually showing S-parameters, itll automatically use a logarithmic
scale.
from empro.toolkit import portparam, graphing
S = portparam.getSMatrix(sim=1)
graphing.showXYGraph(S[1, 1])

If it guesses wrong, or you want to be sure, you can force it by specifying


yUnit="dB"
graphing.showXYGraph(S[1, 1], yUnit="dB")

74

Keysight EMPro Scripting Cookbook

Post-processing and Exporting Simulation Results

Plotting Multiple Datasets


You can plot more than one datasets at once:
graphing.showXYGraph(S[1, 1], S[1, 2], yUnit="dB")

Or just plot the whole matrix:


graphing.showXYGraph(S)

Setting Labels and Titles


The graph title can be set using the title parameter, the axis labels with
xLabel and yLabel. If you want to use custom labels in the legend, you can
supply a list of strings to the names parameter, one string per dataset. Heres an
example using the Microstrip Low Pass Filter example:
s11_fdtd = dataset.getResult(sim=1, run=1, object='Port1',
result='SParameters')
s11_fem = dataset.getResult(sim=2, run=1, object='Port1',
result='SParameters')
graphing.showXYGraph(s11_fdtd, s11_fem, title="FDTD vs FEM",
names=["S11 (FDTD)", "S11 (FEM)"],
xLabel="f", yLabel="S")

Setting Axis Bounds


You can zoom in on the graph by setting xBounds and yBounds to the area of
interest. These parameters expect pairs of floats in reference units. That means
expressions must be evaluated first. For S-parameters, that means you need to
provide limits in linear scale. So say 1 instead of 0 dB:
graphing.showXYGraph(S, xBounds=(0, 10e9),
yBounds=(float(empro.core.Expression("-50 dBa")),
1))

Working with CITI Files


The toolkit contains a citifile module that can be used to read and write CITI
files. It has a class CitiFile that behaves a lot like a ordinary dict, and a
couple of helper functions.

Exporting S-parameters
Exporting just the S-matrix of a simulation can simply be done as follows:
from empro.toolkit import citifile, portparam
citifile.write("C:/tmp/s-matrix.cti", portparam.getSMatrix(sim=1))

Keysight EMPro Scripting Cookbook

75

Post-processing and Exporting Simulation Results

Basically, you can store any combination of dataset or dataset matrices in a CITI
file, as long as they share the same dimensions. Its a matter of populating a
CitiFile object and then saving it. Heres how you can add the port reference
impedance:
citi = citifile.CitiFile(portparam.getSMatrix(sim=1))
for (i,j), zport in portparam.getRefImpedances(sim=1).items():
assert i == j
citi['ZPORT[%s]' % i] = zport
citi.write('c:/tmp/s-matrix.cti')

Importing S-parameters
Importing is almost as easy.
from empro.toolkit import citifile, portparam
citi = citifile.read("C:/tmp/s-matrix.cti")
print citi.keys()
s11 = citi["S[1,1]"]

The asMatrix method recognizes patterns in key names like "S[1,1]" and
groups them into one matrix:
S = citi.asMatrix('S')

Or directly from citifile to XY-graph:


from empro.toolkit import citifile, portparam, graphing
graphing.showXYGraph(citifile.read("C:/tmp/s-matrix.cti").asMatrix('S'),
yUnit="dBa")

Exporting to Touchstone Files


S-parameters can also be exported to Touchstone files using the touchstone
toolkit module. Importing Touchstone files is not supported yet.
from empro.toolkit import touchstone, portparam
touchstone.write("C:/tmp/s-matrix.snp", portparam.getSMatrix(sim=1))

Exporting Surface Sensor Results


You have setup a surface sensor in EMPro, and you want to export the data to a
text file. These sensors already have sampled the data in discrete vertices
(x, y, z) and their DataSets usually have two dimensions: a Frequency or
Time dimension, and VertexIndex. The latter is a dimension of indices over a
list of vertices, which can be retrieved from the topology attribute.
Recipe 5.1 shows a function exportSurfaceSensorData that can export
steady-state data of surface sensors to a tabulated text file. It writes one line per
vertex. The first three columns contain the x, y and z coordinate values. They
76

Keysight EMPro Scripting Cookbook

Post-processing and Exporting Simulation Results

are followed by two columns per frequency containing the field data: real and
imaginary parts.
So exportSurfaceSensorData assumes the dataset is complex-valued, which
is often the case for steady-state data, but it actually has no problem dealing
with real-valued data: float also has real and imag attributes since Python
2.6. Itll just write a lot of zeroes in the imag columns.
In case you want to export transient data, youll want to write a version that
assumes real-valued data, and only write one column per timestep. But thats
left as an exercise for the reader.
The list of vertices is retrieved from the topology attribute. ComplexDataSets
dont have that attribute themselves, but their real and imag parts may have. In
case you want to override that, or if the dataset doesnt has a topology at all, you
can provide your own list of vertices as an optional function argument.
On the first line of the file goes the name and unit of the whole dataset. On the
second line go the column titles. For some, the columnHeader function is used,
described earlier in Helper Functions on page 71.
The idiomatic way to write tabular data to a file or output stream, is to build up a
list line of the different string fields first, and then to join them with tabs into a
single string on line 50. It looks a bit odd to call a method on a string literal, but
most Pythonians will recognize this idiom.
Finally, theres loop over all vertex indices. They are used to retrieve the actual
vertex coordinate from the vertices list. The dimension vertexIndices
actually stores the indices as floats which are not accepted as list indices. So it
is required to cast them to an int explicitly (line 55). Again, a list is build with all
fields for a single line, and then joined to be written to the file.
Recipe 5.1 ExportSurfaceSensor.py
# some helper functions
def columnHeader(name, unit):
return "%s[%s]" % (name, unit.abbreviation())
def strUnit(value, unit):
return str(unit.fromReferenceUnits(value))
def exportSurfaceSensorData(dataset, path, vertices=None):
from empro import core, units, toolkit
# unpack dimensions
if [d.name for d in dataset.dimensions()] != ["Frequency",
"VertexIndex"]:
raise ValueError("dataset must have two dimensions Frequency "
"and VertexIndex, in that order. Please, use "
"datasets from Surface Sensors")
(frequencies, vertexIndices) = dataset.dimensions()
# get all 3D vertices
if not vertices:
try:
topology = dataset.topology
except AttributeError:
try:
topology = dataset.real.topology
except AttributeError:
raise ValueError("dataset must have a topology "
"attribute. Please, use a dataset "
"returned by "

Keysight EMPro Scripting Cookbook

77

Post-processing and Exporting Simulation Results


"empro.toolkit.dataset.getResult")
vertices = topology.vertices
# units for formatting.
freqUnit = empro.units.displayUnits()[units.FREQUENCY]
lengthUnit = empro.units.displayUnits()[units.LENGTH]
dataUnit = empro.units.displayUnits()[dataset.getUnitClass()]
# open file, and write info about datatype.
out = open(path, 'w')
out.write('# %s\n' % columnHeader(dataset.name, dataUnit))
# write column info
line = [columnHeader(x, lengthUnit) for x in ('X', 'Y', 'Z')]
for freq in frequencies:
line += [
"Re,%s %s" % (strUnit(freq, freqUnit),
freqUnit.abbreviation()),
"Im,%s %s" % (strUnit(freq, freqUnit),
freqUnit.abbreviation()),
]
out.write('# %s\n' % '\t'.join(line))
# for each point, write xyz triple +
#
real/imag pairs for each frequency.
for (k, vertexIndex) in enumerate(vertexIndices):
vertex = vertices[int(vertexIndex)]
line = [strUnit(x, lengthUnit) for x in vertex]
for i in range(len(frequencies)):
x = dataset.at(i, k)
line += [strUnit(x.real, dataUnit),
strUnit(x.imag, dataUnit)]
out.write('\t'.join(line) + '\n')
# --- example --if __name__ == "__main__":
E = empro.toolkit.dataset.getResult('C:/tmp/Microstrip_50_Ohm.ep',
sim=4, run=1,
object='Planar Sensor',
timeDependence='SteadyState')
exportSurfaceSensorData(E, 'C:/tmp/test.txt')
print 'done'

Directly Sampling Near Fields (FEM only)


In FEM, near field sensors dont sample on a regular grid like in FDTD, and its
not limited to discrete grid locations either. So, depending on your needs, it may
be more convenient to bring-your-own sample points instead, to directly
evaluate the FEM near fields.
The fem module of the toolkit provides a class NearField that provides the
necessary interface for doing so. In Recipe 5.2 it is shown how to use that to
evaluate the electric field in a single point for all available frequencies.
evaluateElectricFieldInPoint accepts an initialized NearField object and

a 3D coordinate. It iterates over all frequencies and evaluates the electric field.
This results in a triple of complex valuesone for each of the X-, Y- and
Z-componentsand since a real-valued dataset will be returned, the vector
magnitude needs to be computed. When doing so, make sure not to sum the
78

Keysight EMPro Scripting Cookbook

Post-processing and Exporting Simulation Results

vector components as complex numbers, sum the complex magnitudes instead:

2
2
2


e = ex + ey + ez = |ex |2 + |ey |2 + |ez |2
Once all values are retrieved, makeDataSet is used to create a frequency
dimension and to return the data as a single dataset.
Recipe 5.2 DirectSamplingNearFieldFEM.py
def evaluateElectricFieldInPoint(nearfield, x, y, z):
'''
function to evaluate a single near field point for all available
frequencies, returning a dataset.
- nearfield: initialized empro.toolkit.fem.NearField instance
- x, y, z: position in meters
precondition: it is required that nearfield is properly initialized:
- it is a NearField object with a simulation result loaded
- nearfield.excitation is properly set to a port number
'''
import math
from empro.toolkit import dataset
x, y, z = float(x), float(y), float(z) # evaluate expressions.
Es = []
Fs = []
for f in nearfield.frequencies:
Fs.append(f)
nearfield.frequency = f
ex, ey, ez = nearfield.E(x, y, z)
Es.append(math.sqrt(abs(ex) ** 2 + abs(ey) ** 2 + abs(ez) ** 2))
frequency = dataset.makeDataSet(Fs, "Frequency",
unitClass=empro.units.FREQUENCY)
return dataset.makeDataSet(Es, "E(%(x)s,%(y)s,%(z)s" % vars(),
dimensions=[frequency],
unitClass=empro.units.ELECTRIC_FIELD_STRENGTH)
# --- example --if __name__ == "__main__":
from empro.toolkit import fem, graphing
nearfield = fem.NearField(empro.activeProject, "000005")
nearfield.excitation = 1 # = port number
x, y, z = "12 mm", "15 mm", "2 mm"
E = evaluateElectricFieldInPoint(nearfield, x, y, z)
graphing.showXYGraph(E)

Reducing Dataset Dimensions


When dealing with multidimensional datasets like far zone data, you sometimes
want to reduce the number of dimensions by fixating some of them to a certain
value. For example, most far zone field results have three dimensions:
Frequency, Theta and Phi. But XY-plots must be one dimensional: you can
plot the field strength in function of frequency but only for a fixed theta and phi,
you can plot a cross section for all theta if you fix phi and frequency to one value.
Load the EMI Analysis example to get the Far Zone Sensor 3D dataset. To
plot it using showXYGraph you need to reduce the three-dimensional dataset
to a one-dimensional one. You can do that with reduceDimensionsToIndex of
Keysight EMPro Scripting Cookbook

79

Post-processing and Exporting Simulation Results

the dataset module in the toolkit. As first argument, you pass the dataset to
be reduced. After that you pass a number of keyword arguments: the keywords
are the names of the dimensions you want to reduce, and you assign them the
index of the value you want to fix the dimension to. In the following example,
both Theta and Phi are fixed to index 18, which in this case happens to be
90 .
from empro.toolkit import dataset, graphing
fz = dataset.getResult(sim=2, object='Far Zone Sensor 3D',
timeDependence='SteadyState', result='E')
fz2 = abs(dataset.reduceDimensionsToIndex(fz, Theta=18, Phi=18))
graphing.showXYGraph(fz2)

How do you figure out what index values to use for the reduced dimensions?
Heres an interesting Python idiom to find the nearest match in a sequence: use
min to find the minimum error abs(target - x), paired with the value of
interest x. When two pairs are compared, it is done so by comparing them
lexicographically [32]. This means that the pair with the smallest first valuethe
smallest error in this casewill be considered to be the minimum of the list. And
so we get the closest match:
xs = [-1.35, 3.78, -0.44, 1.8, 0.69, 1.33, -3.55, 2.68, -4.78]
target = 1.5
error, best = min( (abs(target-x), x) for x in xs )
print "%s (error=%s)" % (best, error)

This will print:


1.33 (error=0.17)

Using that idiom, findNearestIndex in Recipe 5.3 returns the index of the
nearest value within a dataset. Instead of the value x, its index k within the
dataset is used as the second value of the pairwhich is obviously retrieved
using enumerate [23].
Building upon that, reduceDimensionsToNearestValue is a variation of
reduceDimensionsToIndex. It has a var-keyword parameter **values [14, 28]
to accept arbitrary name=value arguments, where name must be a dimension
name and value an expression to which the dimension must be reduced. Using a
dict comprehension [27] and findNearestNeighbour, it builds a new dictionary
indices where the values are converted to an index within the according
dimension. Finally, it forwards the call to reduceDimensionsToIndex passing
**indices as individual keyword arguments. As an example, both Theta and
Phi are fixed to "90 deg".
Recipe 5.3 ReduceDimensions.py
def findNearestIndex(ds, value):
'''
Searches within the dataset ds for the element nearest to value,
and returns its index within the dataset.
'''
value = float(empro.core.Expression(value))
err, index = min((abs(x - value), k) for (k, x) in enumerate(ds))
return index
def reduceDimensionsToNearestValue(ds, **values):
'''
Similar to dataset.reduceDimensionsToIndex but accepts name=value
keyword arguments where name is the name of a dimension of ds,

80

Keysight EMPro Scripting Cookbook

Post-processing and Exporting Simulation Results


and this dimension will be reduced to the element nearest to value.
'''
from empro.toolkit import dataset
dimensions = { dim.name: dim for dim in ds.dimensions() }
indices = { name: findNearestIndex( dimensions[name], value )
for name, value in values.items() }
return dataset.reduceDimensionsToIndex(ds, **indices)
# --- example --if __name__ == "__main__":
from empro.toolkit import dataset, graphing
fz = dataset.getResult(sim=1, object='3D Far Field',
timeDependence='SteadyState', result='E')
fz2 = reduceDimensionsToNearestValue(fz, Theta="90 deg",
Phi="90 deg")
graphing.showXYGraph(abs(fz2))

Plotting Far Zone Fields


When creating XY or polar graphs, youll find that the graph functions only
accept one-dimensional datasets. Multi-dimensional datasets like far zone fields
are not accepted. In order to plot such data, some work needs to be done to
reduce them to one-dimensional datasets. Recipe 5.4 shows you how you can
accomplish that.
The first part of showFarZoneGraph basically checks if a valid dataset is being
supplied. All steady-state far field datasets have three dimensions: the first is
Frequency and the other two are angular (for example Theta and Phi). The
function is however a bit more liberal than that and also accepts datasets with
one or two dimensions3 . This can occur when you already have reduced a threedimensional far zone dataset to fix one or both of the angles to a specific value.
If only the frequency dimension is present, showFarZoneGraph falls back to a
regular data versus frequency XY plot, see line 28.
Next, on line 34, it searches which of the angleDims dimensions are single
valued. It builds a dictionary indices that maps their names to zero, the only
index possible within a single valued dimension. Later on, this dictionary will be
used to reduce the dataset to get rid of these dimensions. Again, a dict
comprehension is used with a condition: only dimensions with one value will get
an entry.
If indices and angleDims are of the same size, then all angular dimensions are
single valued, and you again have a data versus frequency plot. Use indices
to reduce the dataset to one dimension and show a regular XY graph. The **operator is used to unpack indices as separate keyword arguments [29], since
reduceDimensionsToIndex expects them so.
If the size of indices and angleDims differs more than one, it means that at
least two angular dimensions have more than one value4 . This kind of datasets
cannot be plotted using polar graphs and require a 3D plot. An error is raised.
3 If the dataset has only one dimensions, it must be Frequency. If it has two dimensions, the first must be
Frequency and the second must be an angular dimensions.
4 Or both angular dimensions, since there cannot be more than two.

Keysight EMPro Scripting Cookbook

81

Post-processing and Exporting Simulation Results

By now, its established that theres exactly one meaningful angular dimension,
and you can start making a polar plot over that angle. Theres however still the
frequency dimension to deal with. If theres more than one frequency, a polar
plot should be made per frequency, and they should all be superimposed on one
graph.
The solution to that is of course more reduction. Loop over all frequencies using
enumerate so you get the index as well, add it to indices, reduce the dataset
and add it to the list perFrequency. At the end, that list should consist of one
or more one-dimensional datasets, and you can simply pass that to
showPolarGraph by unpacking it with the *-operator.
Recipe 5.4 ShowFarZoneGraph.py
def strUnit(value, displayUnit):
return "%.2f %s" % (displayUnit.fromReferenceUnits(value),
displayUnit.abbreviation())
def showFarZoneGraph(dataset, **kwargs):
from empro import units
from empro.toolkit.dataset import reduceDimensionsToIndex
from empro.toolkit.graphing import showXYGraph, showPolarGraph
# check if we have proper dimensions.
# the first one should be a frequency, the others angular.
dimensions = dataset.dimensions()
if not dimensions:
raise ValueError("dataset must have at least one dimension")
freqDim, angleDims = dimensions[0], dimensions[1:]
if freqDim.unitClass != units.FREQUENCY:
raise ValueError("first dimensions must be frequency")
if len(angleDims) > 2:
raise ValueError("you can't have more than two angular "
"dimensions")
if any(dim.unitClass != units.ANGLE for dim in angleDims):
raise ValueError("all dimensions except the first must be "
"angular")
# if no angular dimensions, this is just a data vs. frequency plot
if not angleDims:
return showXYGraph(dataset, **kwargs)
# only one angular dimension should have a length greater than 1
# this will be the angle for the polar plots.
# the others should be one angle only, and will be reduced.
# so start by making table of dimensions to be reduced.
indices = { dim.name: 0 for dim in angleDims if len(dim) == 1 }
if len(indices) == len(angleDims):
# all angular dimensions are constant.
# after reduction, this is a regular data vs. frequency plot
reduced = reduceDimensionsToIndex(dataset, **indices)
reduced.unitClass = dataset.unitClass
return showXYGraph(reduced, **kwargs)
elif len(indices) < len(angleDims) - 1:
raise ValueError("only one angular dimension should have a "
"length greater than 1, the other should be "
"constant (one value). You need a 3D plot to "
"visualize this dataset, or you need to "
"reduce it first.")
# now let's make polar plots.
# we need one-dimensional datasets, but we still have two
# dimensions: frequency and angle. Make datasets per frequency and
# add them to one graph.
freqUnit = units.displayUnits()[units.FREQUENCY]
perFrequency = []

82

Keysight EMPro Scripting Cookbook

Post-processing and Exporting Simulation Results


for (index, freq) in enumerate(freqDim):
indices[freqDim.name] = index
reduced = reduceDimensionsToIndex(dataset, **indices)
reduced.name = "%s @ %s" % (dataset.name,
strUnit(freq, freqUnit))
reduced.unitClass = dataset.unitClass
perFrequency.append(reduced)
return showPolarGraph(*perFrequency, **kwargs)
# --- example --if __name__ == "__main__":
from empro.toolkit import dataset
fz = dataset.getResult(sim=2, object='2D Far Zone Field Phi=0deg ',
timeDependence='SteadyState', result='E')
showFarZoneGraph(abs(fz))

Multi-port Weighting
The results available in EMPros Result Browser and from getResult are mostly
single-port excitation results5 . But what do you do if youre interested in the
combined results where more than one port is excited simulatanously?
You take advantage of dataset arithmetic. As explained in Dataset as a Python
Number on page 66, DataSet supports many of the arithmetic operations that
can be applied to regular Python numbers. You can add, multiply or scale
datasets. You can also sum a list of datasets6 . So you have everything at your
disposal to do linear combinations.
getWeightedResult of Recipe 5.5 demonstrates this, and acts as a replacement
for getResult where the run parameter is replaced by runWeights: a

dictionary of run:weight pairs. It then loops over these pairs, gets the single-port
result and scales it, and sums everything at the end. Its simple enough to fit in a
single statement, apart from an import and argument validation. **kwargs
again acts like a passthrough dictionary, so that getWeightedResults accepts
additional keyword arguments like result='E' which are simply passed to
getResult.

NOTE

Whenever you weight vector field data, make sure you weight the
separate complex vector components7 , not the magnitudes.

Combining it with Recipe 5.3 and Recipe 5.4, its also easy to plot multi-port far
field data. Here, the Theta and Phi vector components are combined
separately, because otherwise the phase would not correctly be taken into
account. Once you have the weighted components, you can compute the vector
5 This is true for FEM and most of FDTD simulations. The exception are FDTD simulations where you dont
compute S parameter results, so that more than one port can be active in one run.
6 You can also apply sum to a dataset directly, but that will compute the sum of all its values. The sum of
a list of datasets will yield a new dataset with the element-by-element sums.
7 Theta and Phi components for far fields; X, Y and Z for near fields.

Keysight EMPro Scripting Cookbook

83

Post-processing and Exporting Simulation Results

magnitude easily as explained in Directly Sampling Near Fields (FEM only) on


page 78.
eFields = [getWeightedResult(sim=1, runWeights=runWeights,
object='Far Zone Sensor',
timeDependence='SteadyState',
result='E',
component=component)
for component in ('Theta', 'Phi')]
eMagnitude = dataset.sqrt(sum(abs(e) ** 2 for e in eFields))
eMagnitude.unitClass = empro.units.ELECTRIC_FIELD_STRENGTH
showFarZoneGraph(reduceDimensionsToNearestValue(eMagnitude,
Theta="90 deg"))

Recipe 5.5 GetWeightedResult.py


def getWeightedResult(context=None, runWeights=None, **kwargs):
from empro.toolkit import dataset
if not runWeights:
raise ValueError("You must supply a dictionary of run:weight "
"pairs")
return sum(weight * dataset.getResult(context, run=run, **kwargs)
for (run, weight) in runWeights.iteritems())
# --- example (using Keysight Phone) --if __name__ == "__main__":
runWeights = {
1: 0.75, # GSM Antenna
2: 0.25, # Bluetooth Antenna
}
eff = getWeightedResult(sim=1,
runWeights=runWeights,
object='System',
timeDependence='SteadyState',
result='RadiationEfficiency')
for e, f in zip(eff, eff.dimension(0)):
print f, ":", e

Maximum Gain, Maximum Field Strength


Youve computed far zone fields all around your antenna, but youre only
interested in the maximum gain per frequency. You can find this number in the
3D plots of the far field, but how do you get in Python?
maxPerFrequency in Recipe 5.6 will help with that task. It takes advantage of
the fact that you can call the max operator on any dataset to retrieve its
maximum value:
gain = dataset.getResult(sim=2, object='3D Far Field',
timeDependence='SteadyState',
result='Gain')
print max(gain)

But since you want to know the maximum value per frequency, youll first have
to reduce the dataset to each frequency. Doing that with a list comprehension
results in:
freq = gain.dimension(0)
print [max(dataset.reduceDimensionsToIndex(gain, Frequency=index))
for index,f in enumerate(freq)]

84

Keysight EMPro Scripting Cookbook

Post-processing and Exporting Simulation Results


maxPerFrequency slightly generalizes that by first looking which of the

dimensions is the frequency axis. Its not necessarely the first, and its not
necessarely called Frequency, but it should have the FREQUENCY unit class.
There should be exactly one of course, and an exception is raised when this is
not the case. Because you cannot use a variable as a keyword arguments name,
a literal dictionary {freq.name: index} is unpacked as keyword argument
instead [29].
Once a list of the maximum values has been computed, it is transformed into a
dataset using makeDataSet. The original frequency dimension is attached to it,
but it is cloned to prevent it being destroyed when the original dataset ds goes
out of scope.

NOTE

Whenever you reuse an existing dimension to attach it to a newly created DataSet,


you should clone it first. Normal Pythons object lifetime does not count
here, and the original dimension is destroyed when its original parent is.

maxPerFrequency only deals with real numbers, so if you want to compute the
maximum electrical field, you should add complexPart='ComplexMagnitude'

to the query:
eField = dataset.getResult(sim=2, object='3D Far Field',
timeDependence='SteadyState',
result='E', complexPart='ComplexMagnitude')
maxEField = maxPerFrequency(eField)
graphing.showXYGraph(maxEField)

Or take advantage of the abs operator:


eField = dataset.getResult(sim=2, object='3D Far Field',
timeDependence='SteadyState',
result='E')
maxEField = maxPerFrequency(abs(eField))
graphing.showXYGraph(maxEField)

Recipe 5.6 MaxPerFrequency.py


def maxPerFrequency(ds):
from empro import units
from empro.toolkit.dataset import makeDataSet, \
reduceDimensionsToIndex
freqDims = [dim for dim in ds.dimensions()
if dim.unitClass == units.FREQUENCY]
if len(freqDims) != 1:
raise ValueError("dataset should have exactly one frequency "
"dimension")
freq = freqDims[0]
maxValues = [max(reduceDimensionsToIndex(ds,
**{freq.name: index}))
for index,f in enumerate(freq)]
return makeDataSet(maxValues,
dimensions=[freq.clone()],
name="Max(%s)" % ds.name,
unitClass=ds.unitClass)
# --- example --if __name__ == "__main__":

Keysight EMPro Scripting Cookbook

85

Post-processing and Exporting Simulation Results


from empro.toolkit import dataset, graphing
gain = dataset.getResult(sim=2, object='3D Far Field',
timeDependence='SteadyState',
result='Gain')
maxGain = maxPerFrequency(gain)
graphing.showXYGraph(maxGain)

Integration over Time


Suppose you have a time dependent DataSet, and you want to integrate it. It
could be a one-dimensional signal like instantaneous power, or maybe a
two-dimensional signal like the transient Poynting vectors on a surface sensor.
Recipe 5.7 shows you timeIntegrate that helps with this task. Applying it to a
one-dimensional transient signal, simply returns a number:
p = empro.toolkit.dataset.getResult(sim=1, run=1, object='Port1',
result='InstantaneousPower')
print timeIntegrate(p)

Integrating Poynting vectors will reduce the dataset dimensionality from two to
one, eliminating Time and leaving VertexIndex. However, if youre integrating
vector data, you must be carefull to integrate the components seperately. Failing
to do so will result in integrating the vector magnitude instead, which will yield
the wrong result:
sx, sy, sz = [dataset.empro.toolkit.getResult(sim=1, run=1,
object='Surface Sensor',
result='S',
component=comp)
for comp in ('X', 'Y', 'Z')]
Sx, Sy, Sz = [timeIntegrate(s) for s in (sx, sy, sz)]

At the center of timeIntegrate, you can find helper function integrate. It


accepts a one-dimensional dataset dswhich of course should be time
dependentand a sequence dts of timestep deltas. The function simply uses
the rectangle method to integrate the signalmore advanced methods are left as
an excercise to the readerwhich is fully implemented on line 16 as a single sum
statement fed by a generator expression [15]. Using a generator expression
instead of a list comprehension avoids have to build a list of d * dt products in
memory, only to iterate over it again to sum all its values. Instead, sum will now
add the products while they are computed. For the same reason, izip of
itertools [24] is being used instead of the regular zip, because the latter too
builds a new list in memory while izip does everything on the fly.
dts itself is generated as a listso we can iterate over it several timesand is
simply the difference between the current and the next timestep. xrange is the
iterator version of range, and again used to avoid creating the long list of indices

in memory.
Depending of the number of the other dimensions, dataset will either be
directly integrated to a single number, of a new dataset will be generated. In the
latter case, you simply enumerate over the other dimension, each time reducing
dataset and integrate it, storing the result in a list. A nice list comprehension
will do of course. Using makeDataSet, you turn that list into a DataSet,
86

Keysight EMPro Scripting Cookbook

Post-processing and Exporting Simulation Results

attaching the other dimension and setting the right unit class. other is cloned
to avoid scoping issues, as explained in Maximum Gain, Maximum Field
Strength on page 85.
Recipe 5.7 TimeIntegration.py
def timeIntegrate(dataset):
'''
Takes some Time dependent dataset and integrate over time.
Returns a dataset without Time dimension, only with VertexIndex
dimension
'''
from empro import units
from empro.toolkit.dataset import makeDataSet, \
reduceDimensionsToIndex
from itertools import izip
def integrate(ds, dts):
assert len(ds) == len(dts)
assert len(ds.dimensions()) == 1
assert ds.dimension(0).unitClass == units.TIME
return sum(d * dt for (d, dt) in izip(ds, dts))
# make sure there's exactly one time dimension, and get it.
timeDims = [dim for dim in dataset.dimensions()
if dim.unitClass == units.TIME]
if len(timeDims) != 1:
raise ValueError("expects a dataset with exactly one time "
"dimension.")
time = timeDims[0]
# compute time deltas.
dts = [time[k+1] - time[k] for k in xrange(len(time) - 1)]
dts.append(dts[-1])
otherDims = [dim for dim in dataset.dimensions()
if dim.unitClass != units.TIME]
if not otherDims:
return integrate(dataset, dts)
elif len(otherDims) == 1:
other = otherDims[0]
result = [integrate(reduceDimensionsToIndex(dataset,
**{other.name: i}),
dts)
for i,_ in enumerate(other)]
return makeDataSet(result, id="Int(%s)" % dataset.name,
dimensions=[other.clone()],
unitClass=dataset.unitClass)
else:
raise ValueError("datasets with more than two dimensions are "
" not supported.")

Exporting Arbitrary Datasets to CSV Files


What about a general function that can export arbitrary datasets of arbitrary
dimensions to a general format? The Python Standard Library contains a module
csv to work with comma-separated values (CSV) files [21, 19]. Recipe 5.8 shows
an function that uses that module to export any number of datasets to a CSV
file. So you can export just one dataset, or ten, or a whole S-matrix. They can
be real, complex, or a mixture of both. They can be of different unit classes, so
you can mix voltage and current data.
Keysight EMPro Scripting Cookbook

87

Post-processing and Exporting Simulation Results

All datasets must share the same dimensions though: they must have the same
number of dimensions; their dimensions should have the same names and unit
classes, and should be listed in the same order; and the dimensions should be
sampled identically.
Although the implementation of the function uses rather advanced Python
concepts, using it is very simple. Heres how you can export both a voltage and
current dataset to one file:
# exporting two datasets of different result types
v = empro.toolkit.dataset.getResult(sim=2, object='Feed', result='V')
i = empro.toolkit.dataset.getResult(sim=2, object='Feed', result='I')
exportDatasetsToCSV("C:\\tmp\\test1.csv", v, i)

Exporting matrices is equally simple. In the following example, its also


demonstrated you can use the dialect argument to specify a format that uses
tab delimiters instead of commas. See [21] for more dialect parameters.
# exporting matrices, using a CSV dialect with tabs as delimiter.
from empro.toolkit import portparam
s = portparam.getSMatrix(sim=1)
zref = portparam.getRefImpedances(sim=1)
exportDatasetsToCSV("C:\\tmp\\test2.csv", s, zref, dialect="excel-tab")

Function Definition with Arbitrary (Keyword) Parameters


Recipe 5.8 shows an advanced function exportDatasetsToCSV that accepts a
file path and an arbitrary number of datasets as arguments. It also accepts an
optional keyword argument names with a list of column names to be used for
each of the datasets.
The parameter definition with the * and ** notations may look unfamiliar to you.
Heres whats going on using the terminology of [14]: exportDatasetsToCSV
first defines a positional-or-keyword parameter path and a var-positional
parameter *datasets. This will result in the first positional argument to be
assigned to path, and all positional arguments following to be gathered as a
tuple in datasets. Because no positional-or-keyword parameter for names can
follow on a var-positional one, a var-keyword parameter **kwargs is used that
gathers undefined keyword arguments. When passing a names argument, it will
register as a dictionary entry in kwargs, which can be picked up later.
The above description is a very technical one. Lets try to illustrate this with a
simple example:
def func(first, *args, **kwargs):
print "first:", first
print "args:", args
print "kwargs:", kwargs
func("spam", "bacon", "eggs", "spam", extra="spam")

The first positional argument "spam" will be assigned to first, the others are
gathered as a tuple args. The keyword argument extra="spam" will be stored
in the dictionary kwargs, and the output will be:
first: spam
args: ('bacon', 'eggs', 'spam')

88

Keysight EMPro Scripting Cookbook

Post-processing and Exporting Simulation Results


kwargs: {'extra': 'spam'}

In this manner, exportDatasetsToCSV extracts the names argument from


kwargs on line 7. pop works similar to get, except it also removes the entry from
the dictionary [20]. This makes sure that the excess can be passed as **kwargs
to cvs.writer so that CSV dialect parameters [22] can also be passed as
optional arguments to exportDatasetsToCSV. If names wouldnt be removed
from the dictionary, cvs.writer would complain about an unknown keyword
argument. pop also takes a default value so that if no names argument is given,
the dataset names are used instead.

Boiling Down Inputs to a Real-valued DataSet List


exportDatasetsToCSV can receive real or complex datasetsor even whole

matricesbut the CSV columns should only store floating point values8 . This
means that the each ComplexDataSet needs to be be replaced by its real and
imaginary subparts, and matrices should be replaced by their individual
elements. Thats the task of unwrapDatasets. It will generate a list of
real-valued DataSet/name pairs that can be directly exported to the CSV file.
unwrapDatasets takes a list of datasets and names, and returns a single list
unwrapped of real-valued dataset/name pairs. It loops over each dataset/name

pair and checks if the dataset is something more complicated than a real-valued
DataSet.
If its a matrix, it will build a new list of datasets and names, subsets and
subnames for all of the elements of the matrix, and it will recursively call
exportDatasetsToCSVso that complex elements can be unwrapped againand
concatenates the result to unwrapped.
If not a matrix, it tests for the existence of the real and imag attributes. If an
AttributeError is raised, they dont exist and it is concluded that dataset is a
regular real-valued DataSet. Its simply appended to unwrapped, together with
its name. If real and imag do exist, it must have been a ComplexDataSet, and
both parts are appended to unwrapped separately.
The zip(*...) construction on line 11 is a Python idiom known as unzip [34].
unwrapDatasets returns a single list of dataset/name pairs, but you really want
a list of datasets and a list of names. So you unzip by using the * syntax to feed
the pairs as individual arguments to zip.9

Writing to the CSV File


Whats still left is creating the actual CSV file. On line 17, a writer is initialized
with the excess keyword arguments **kwargs.
8 Well, thats technically not true, CSV data can be anything you want, but limiting yourself to floating point
value will make it a lot easier to handle the data in external programs.
9 If you think of matrices as lists of lists, then this is basically transposing the matrix, and this idiom is often
used for that too.

Keysight EMPro Scripting Cookbook

89

Post-processing and Exporting Simulation Results

On the first line of the file goes a row with column headers which we create from
the field names and units. The way columnHeader is used for this is explained
before. Just keep in mind you not only need to store the datasets, but also the
dimensions.
The interesting bit is how enumerateFlat is used to iterate over all dimensions
at once. The product function of the itertools module [24]. It takes a number
of distinct sequences (like dimensions), and iterates over every possible
combination of values, like in a nested loop. Heres an example with two simple
Python lists.
from itertools import product
print list(product([10, 20, 30], [1, 2]))

This will result in the following list of pairs:


[(10, 1), (10, 2), (20, 1), (20, 2), (30, 1), (30, 2)]

If you compare this to the way flat indices iterate over datasets in
Multidimensional Datasets on page 66, youll see this happens in the exact
same order. So, that means you can use the product of the dataset dimensions,
feed it through enumerate [23], and youll be iterating over the flat index of the
dataset.
So you use enumerateFlat to iterate over all dimensions, and each time you get
the flat index k which you can use to retrieve the actual data values from the
datasets, and dimVal which is a tuple of the actual values of the dimensions
for that record. The dimVal tuple is converted to a list so that the list
comprehension can be added to it, and the record is written to the CSV file.
Recipe 5.8 ExportToCSV.py
def exportDatasetsToCSV(csvfile, *datasets, **kwargs):
from empro import units
import csv
import codecs
dimensions = datasets[0].dimensions()
names = kwargs.pop("names", [ds.name for ds in datasets])
assert len(datasets) == len(names), \
"You should supply exactly the same number of names as datasets"
datasets, names = zip(*unwrapDatasets(datasets, names))
fieldnames = [dim.name for dim in dimensions] + list(names)
fieldunits = [units.displayUnits()[x.unitClass]
for x in (dimensions + datasets)]
writer = csv.writer(open(csvfile, 'wb'), **kwargs)
writer.writerow([encodeUtf8(columnHeader(name, unit))
for name, unit in zip(fieldnames, fieldunits)])
for (k, dimVal) in enumerateFlat(dimensions):
fields = list(dimVal) + [ds[k] for ds in datasets]
writer.writerow([unit.fromReferenceUnits(x)
for x, unit in zip(fields, fieldunits)])
def unwrapDatasets(datasets, names=None):
from empro.toolkit.dataset import DataSetMatrix
unwrapped = []
for ds, name in zip(datasets, names):
if isinstance(ds, DataSetMatrix):

90

Keysight EMPro Scripting Cookbook

Post-processing and Exporting Simulation Results


keys = sorted(ds.keys())
subsets = [ds[key] for key in keys]
if ds.isDiagonal():
subnames = ["%s[%s]" % (name, r) for (r, c) in keys]
else:
subnames = ["%s[%s,%s]" % (name, r, c)
for (r, c) in keys]
unwrapped += unwrapDatasets(subsets, subnames)
else:
try:
re, im = ds.real, ds.imag
except AttributeError:
unwrapped.append((ds, name))
else:
unwrapped += [
(re, "Re(%s)" % name),
(im, "Im(%s)" % name),
]
return unwrapped
def enumerateFlat(dimensions):
from itertools import product
return enumerate(product(*dimensions))
def columnHeader(name, unit):
if unit.abbreviation():
return "%s[%s]" % (name, unit.abbreviation())
else:
return name
def strUnit(value, unit):
return str(unit.fromReferenceUnits(value))
def encodeUtf8(x):
if isinstance(x, unicode):
return x.encode("utf8")
return x

Exporting Surface Sensor Topology to OBJ file


Youve exported the near field data of a surface sensor so that you can visualize
it in another tool, but you also need to accompanying surface geometry.
Wavefront OBJ [36] is a simple 3D geometry definition file format and widely
supported, which makes it ideal for this purpose.
As mentioned in Exporting Surface Sensor Results on page 76, surface sensor
results have a topology property, so you can export that. It has lists of vertices,
vertex normals and facets:
Jc = dataset.getResult(sim=1, object='Surface Sensor', result='Jc')
topology = Jc.topology
print "verts:", len(topology.vertices), topology.vertices[:10], '...'
print "normals:", len(topology.vertexNormals), \
topology.vertexNormals[:10], '...'
print "facets:", len(topology.facets), topology.facets[:10], '...'

There are as many normals as vertices, and both lists contain (x, y, z) triplets.
Keysight EMPro Scripting Cookbook

91

Post-processing and Exporting Simulation Results

Each facet is a tuple of indices which refer into the vertex and normal lists10 .
The OBJ file format is a plain text format, so its just a matter of writing all
vertices, normals and facets to the file. Vertices are just three numbers per line,
separated by whitespace and prepended by v. Heres the eight vertices of a
cube:
v
v
v
v
v
v
v
v

-1
-1
-1
-1
+1
+1
+1
+1

-1
-1
+1
+1
-1
-1
+1
+1

-1
+1
-1
+1
-1
+1
-1
+1

Each vertex gets an implicit one-based index: the first vertex of the file gets
index 1, the second 2, and so on.
Vertex normals are likewise written to the file, but each line starts with vn
instead of v:
vn
vn
vn
vn
vn
vn

-1 0 0
+1 0 0
0 -1 0
0 +1 0
0 0 -1
0 0 +1

For a facet, the line starts with f and is then followed by the indices of each of
its vertices. Heres how the six faces of the cube are encoded:
f
f
f
f
f
f

1
3
7
5
1
2

2
4
8
6
3
6

4
8
6
2
7
8

3
7
5
1
5
4

If each facet vertex also has a normal, you write it next to the vertex index,
separated with a double slash:
f
f
f
f
f
f

1//1
3//4
7//2
5//3
1//5
2//6

2//5
4//4
8//2
6//3
3//5
6//6

4//1
8//4
6//2
2//3
7//5
8//6

3//1
7//4
5//2
1//3
5//5
4//6

Putting all this together results in exportToOBJ of Recipe 5.9. The vertex
coordinates are stored in display units. The normals are stored in reference units
as they are normalized. The vertex indices of the facets need to be incremented
by one, to translate from zero-based to one-based indexing.
Recipe 5.9 ExportToOBJ.py
def exportToOBJ(path, topology):
'''
export topology in Wavefront OBJ format (because it's a simple
format)
NOTE: indices are one-based! faces index in the vertex and normal
arrays, but they start counting from one.
'''
10 Vertices

92

and normals are ordered in the same way, so the same index is used for both lists.

Keysight EMPro Scripting Cookbook

Post-processing and Exporting Simulation Results

lengthUnit = empro.units.displayUnits()[empro.units.LENGTH]
strLength = lambda x: str(lengthUnit.fromReferenceUnits(x))
with file(path, "w") as out:
out.write("# vertices [%s]\n" % lengthUnit.abbreviation())
for v in topology.vertices:
out.write("v %s\n" % " ".join(map(strLength, v)))
out.write("# normals\n")
for vn in topology.vertexNormals:
out.write("vn %s\n" % " ".join(map(str, vn)))
out.write("# faces\n")
for facet in topology.facets:
out.write("f %s\n" % " ".join("%d//%d" % (i+1, i+1)
for i in facet))
def columnHeader(name, unit):
return "%s [%s]" % (name, unit.abbreviation())
def strUnit(value, unit):
return str(unit.fromReferenceUnits(value))
# --- example --if __name__ == "__main__":
Jc = empro.toolkit.dataset.getResult(sim=1, object='Surface Sensor',
timeDependence='SteadyState',
result='Jc')
exportToOBJ("C:/tmp/plane.obj", Jc.real.topology)

Keysight EMPro Scripting Cookbook

93

94

Post-processing and Exporting Simulation Results

Keysight EMPro Scripting Cookbook

Keysight EMPro Scripting Cookbook

6
Extending EMPro with Add-ons
Hello World!

95

Adding Dialog Boxes: Simple Parameter Sweep 97


Extending Context Menu of Project Tree: Cover Wire Body 100

Since 2012, EMPro provides an add-on mechanism that allows you to easily
extend the GUI with new functionality that can be written or customized by
yourself. Before, one had to copy/paste or import Python scripts in every project
you wanted to use it, select the right script in the Scripting editor, and press
play. With the new add-on mechanism, it becomes possible to insert new
persistent commands in the Tools menu or in the Project Trees context menu.
Given the nature of this cookbook, it should come as no surprise that these
add-ons must be written as Python modules. In this chapter, it is shown how to
create one.
The Keysight Knowledge Center has a download section where you can find
additional add-ons. Take a look at their source code to see how they work, it
may help to build your own add-on. Or take an existing one, and modify it for
your own purposes. When youve created an add-on that you think may be
useful for others, you can submit it on the knowledge center so that we may
make it available as a user contributed add-on.
www.keysight.com/find/eesof-empro-addons

Hello World!
To get your feet wet, this chapter starts with the Hello World of the add-ons, to
demonstrate the basic elements every add-on should have. Recipe 6.1 shows a
minimal implementation that will add a new command Tools > Hello World
showing a simple message.
The meat and mead of this example add-on is the function helloWorld defined
on lines 8 to 10. Calling this function will cause a message box to appear saying
Hi There ... In a real world case, you would of course have something more
usefull instead.
95

Extending EMPro with Add-ons

The helloWorld function by itself would already make a nice Python module,
but its not an add-on yet. The missing elements are shown one by one.

Documentation
To document add-ons, you simply use docstrings [11] which are Pythons natural
mechanism to document modules, classes, or functions:
'''
Documenting our Hello World Add-on
'''

To add one, simply write a string literal as the first statement in your module.
Here, the documentation is a triple quoted blockstring on lines 1 to 3. Although
normal string literals will do just fine, most docstrings will be blockstrings
because they naturally allow multilined strings. No need to insert /n between
lines.

Author and version


For the author and version metadata of the add-on, the practice of assigning the
__author__ and __version__ variables is adopted:
__author__ = "John Doe"
__version__ = "1.0"

Although neither are standard and are entirely optional, they are commonly used
conventions. If youre interested, theyre both referred to in the documentation
of Epydoc [8], the usage of the __version__ convention is also documented in
PEP 396 [12].

Add-on definition
Each add-on is required to have an entry-point function _defineAddon.
It should not have any parameters, and it must return an instance of
empro.toolkit.addon.AddonDefinition. While the documentation, author
and version are entirely optional, without the _defineAddon function your
python script will not be recognized as a proper add-on.
def _defineAddon():
from empro.toolkit import addon
return addon.AddonDefinition(
menuItem=addon.makeAction('Hello World', helloWorld)

As you can see, almost the whole of _defineAddon exists of a single return
statement that returns the add-on definition (lines 14 to 16). The only other
statement is to import the addon module (line 13). This will be typical for most
add-ons.
96

Keysight EMPro Scripting Cookbook

Extending EMPro with Add-ons

For this example, an AddonDefinition with a single menuItem is returned, for


which the python function helloWorld defined on lines 8 to 10 is wrapped as an
action using empro.toolkit.addon.makeAction. The first argument of
makeAction is the caption so that the menu item will show up as Tools > Hello
World. When clicked, it will call the function helloWorld, showing a simple
message box.
Recipe 6.1 HelloWorld.py
'''
Documenting our Hello World Add-on
'''
__author__ = "John Doe"
__version__ = "1.0"
def helloWorld():
from empro import gui
gui.MessageBox.information("Hello World!", "Hi There ...",
gui.Ok, gui.Ok)
def _defineAddon():
from empro.toolkit import addon
return addon.AddonDefinition(
menuItem=addon.makeAction('Hello World', helloWorld)
)

Adding Dialog Boxes: Simple Parameter Sweep


Most add-ons will probably require some sort of a dialog box for the user to
enter some parameters. This is demonstrated in Recipe 6.2.
showParameterSweepDialog starts by instantiating a new SimpleDialog with

two command buttons, of which the OK button is renamed. A label is added on


line 28 to display some help instructions for the user. Instead of repeating
yourself and retyping the add-ons documentation, you can simply reuse the
modules docstring. Thats easy enough, because the special variable name
__doc__ contains it. Just strip the whitespace and youre done. Next, a
drop-down list is added filled with the names of all the editable parameters.
Finally, three ExpressionEdit fields are added which are similar to a normal
text edit field but optimized for editing expressions.
Each time we select another parameter, youd like to update the unitClass of
the expression editors and preserve the display value while doing so. If the
current unit is mm and you enter the value 10, youll see 10 mm. Its display
value is 10, but its real value in references units (meters) is 0.01. If you simply
change the unit class to frequency with GHz as display unit, youll get
1e-11 GHz instead of the 10 GHz as you would have expected. To fix that, in
onParameterSelected you first check if the expressions formula is a literal
number by feeding it to the built-in float operator (line 71). Any more complex
formula with operators and units will raise a ValueError and is simply ignored.
If it succeeds however, you now have the value in reference units. Convert it to
display units with fromReferenceUnits. Then, convert it back to the reference
units of the new unit class with toReferenceUnits and set it as the new
Keysight EMPro Scripting Cookbook

97

Extending EMPro with Add-ons

formula. Call onParameterSelected once on line 81 to initialize the unit classes


at the start.
When the dialog is dismissed, onFinished is called to perform the actual
sweep. simulateParameterSweep of the simulation toolkit module expects
the parameters to sweep over as keyword arguments [28]. For example, if you
would want to sweep over parameters foo and bar, youd type something like
simulateParameterSweep(foo=[1, 2], bar=[3, 4]). However, that only
works if you know the parameter names when writing the script. In this case, the
parameter name is variable, so you cant do that. The solution on line 96 is to
build a dictionary of (parameter name, value sequence) pairs and unpack it as
keyword arguments using the **-operator [29].
sequence is somewhat similar to Pythons own range() function but not quite.
It only supports the three-argument version, and in contrary to range(), stop

will always be part of the generated sequence. Instead of returning a simple


list, it returns a generator over which can be iterated [33]. For a more detailed
discussion of floating-point range replacements, see [1].
Recipe 6.2 SimpleParameterSweep.py
'''
An example add-on performing a sweep over a single parameter,
demonstrating how to add new dialogs to EMPro.
To use it, you select the Parameter over which you want to sweep,
and set the Start, Stop and Step values (which may be expressions).
Finally, you click on Create & Queue Simulations.
A new simulation will be created for each of the values in the range
Start + k * Step for k = 0,1,2,... until Stop is reached.
'''
__author__ = "Keysight Technologies, Inc."
__version__ = "1.0"
def showParameterSweepDialog():
import empro
from empro import core, gui, units
dialog = gui.SimpleDialog(gui.Ok | gui.Cancel)
dialog.windowFlags &= ~gui.WF_WindowStaysOnTopHint
dialog.title = "Simple Parameter Sweep"
# rename the OK button to something more meaningfull.
dialog.setButtonText(gui.Ok, "Create && Queue Simulations")
layout = dialog.layout
layout.add( gui.Label( __doc__.strip() ))
# for the parameter selector, add a label and a combobox.
parameterWidget = gui.Widget()
parameterLayout = parameterWidget.layout = gui.HBoxLayout()
parameterLayout.spacing = 0
parameterLayout.addWidget( gui.Label("Parameter"))
parameterCombo = gui.ComboBox()
parameterLayout.addWidget( parameterCombo )
layout.add(parameterWidget)
# fill the combo box with all editable parameters.
parameters = empro.activeProject.parameters()
for name in parameters.names():
if not parameters.isEditable(name):
continue
parameterCombo.addItem(name)

98

Keysight EMPro Scripting Cookbook

Extending EMPro with Add-ons

# add three expression editors.


startEdit = gui.ExpressionEdit(0, units.SCALAR, "Start ")
layout.add(startEdit)
stopEdit = gui.ExpressionEdit(10, units.SCALAR, "Stop ")
layout.add(stopEdit)
stepEdit = gui.ExpressionEdit(1, units.SCALAR, "Step ")
layout.add(stepEdit)
editors = (startEdit, stopEdit, stepEdit)
def onParameterSelected(index):
'''
If possible, change unit of editors to that of new parameter,
but preserve the display value so that 1 mm becomes 1 GHz
instead of 1e-12 GHz
'''
# figure out unit class by running it through an Expression.
parameter = parameterCombo.itemText(index)
formula = empro.activeProject.parameters().formula( parameter )
unitClass = core.Expression( formula ).unitClass()
unit = units.displayUnits()[unitClass]
for edit in editors:
if edit.unitClass() == unitClass:
continue
try:
refValue = float(edit.expression().formula())
except ValueError:
pass # formula is more than just a literal, ignore
else:
oldUnit = units.displayUnits()[edit.unitClass()]
displayValue = oldUnit.fromReferenceUnits(refValue)
edit.setExpression(unit.toReferenceUnits(displayValue))
edit.setUnitClass(unitClass)
parameterCombo.onCurrentIndexChanged = onParameterSelected
onParameterSelected(0) # run it once, to init the editors.
def onFinished(code):
'''
When Create & Queue Simulations is clicked, perform sweep.
'''
if code != dialog.Accepted:
return
from empro.toolkit.simulation import simulateParameterSweep
# translate expressions to floating point values
(start, stop, step) = [float(edit.expression())
for edit in editors]
# pass dict of parameter:sequence pair as keyword arguments
parameter = parameterCombo.currentText()
kwargs = {parameter : list(sequence(start, stop, step))}
simulateParameterSweep(**kwargs)
dialog.onFinished = onFinished
dialog.show(True)
def sequence(start, stop, step):
'''
Somewhat similar to Python's range(), but for floating point.
In contrary to range(), stop will be included in the sequence.
'''
from itertools import count
for k in count():
value = start + k * step
if value >= (stop - .05 * step):
# exit loop when value reaches stop within 5% of step
yield stop
return
yield value

Keysight EMPro Scripting Cookbook

99

Extending EMPro with Add-ons

def _defineAddon():
from empro.toolkit import addon
return addon.AddonDefinition(
menuItem=addon.makeAction('Simple Parameter Sweep',
showParameterSweepDialog,
icon=":/application/ParameterSweep.ico")
)

Extending Context Menu of Project Tree: Cover Wire Body


Add-ons can also be used to add new menu items to the context menu of the
items in the Project Tree. To demonstrate how, the function sheetFromWireBody
from Recipe 2.3 is taken and turned it into an add-on so it can be applied to any
Wire Body from the user interface.
coverWireBody is the heart of the add-on. Its role is similar to
coverAllWireBodies, but it only replaces a single Wire Body. It searches the
projects geometry for the Assembly containing wirebody on line 34. Then, it
figures out the index within assembly so it can replace the original part by the
covered copy. If it fails to find the index, a ValueError exception is raised, and
covered is simply appended.
_doCoverWireBody wraps coverWireBody so that it can be called as an menu
action. It takes one parameter selection that will hold the list of selected items

when called from the context menu. It is made optional so that the same
function can double as an action for the Tools menu. If no arguments are passed,
selection will be None, in which case it defaults to the list of selected items
from the globalSelectionList (line 49). The function goes on to verify that
exactly one Wire Body is selected and finally calls coverWireBody.
By splitting the functionality over two functions coverWireBody and
_doCoverWireBody, the former function can easily be reused. When the add-on
is enabled, it can be imported as module empro.addons.CoverWireBody so
that the functions sheetFromWireBody and coverWireBody can easily be called
from other scripts. The leading underscore of _doCoverWireBody is
a convention used to indicate that a function is considered a private
implementation detail, and not usefull for others.
Context menus need to be populated based on the context, so instead of simply
defining menu items, you need to supply some logic to analyze the context. This
needs to come in the form of a function that will be called each time the context
menu is to be shown, and it needs to compute which menu items to add. In this
recipe, that function is _onContextMenu. It takes two parameters: the list of the
selected items, and the types of these items as a set. The former is the same as
for _doCoverWireBody, but the latter might require some more explanation. Say
you have four Wire Bodies and two Assemblies selected in the user interface.
So selection will be a list of six items. To know if any of them is a Wire Body,
youd need to iterate over each item and test its type:
hasSketch = any(isinstance(x, geometry.Sketch) for x in selection)

100

Keysight EMPro Scripting Cookbook

Extending EMPro with Add-ons

This is however a linear operation, and predicates like this must be efficient:
theyre called every time the context menu is shown. You dont want heavy
operations there. To avoid that, the set of the select types is provided as an extra
parameter. In the example above, selectedTypes will contain two elements:
geometry.Sketch and geometry.Assembly. Checking if any of the selected
items is a Wire Body simply becomes a containment test:
hasSketch = geometry.Sketch in selectedTypes

Once it is determined the context is right, a menu item is created using


addon.makeContextAction and returned to be inserted in the menu. Its a
different maker than for regular menu items because of the selection parameter.
If nothing (or None) is returned, no menu items will be inserted.
The last thing you need to do to get the context menu working, is to define it
properly in _defineAddon, by setting onContextMenu. menuItem is also defined
so that you get a regular menu item in Tools too.
Recipe 6.3 CoverWireBody.py
'''
A simple add-on converting a Wire Body into a Sheet Body, demonstrating
how to add new items to the Project Tree's context menu.
'''
__author__ = "Keysight Technologies, Inc."
__version__ = "1.0"
def sheetFromWireBody(wirebody, name=None):
'''
Creates a Sheet Body by covering a Wire Body.
A new Model is returned.
wirebody is cloned so the original is unharmed.
'''
from empro import geometry
model = geometry.Model()
model.recipe.append(geometry.Cover(wirebody.clone()))
model.name = name or wirebody.name
return model
def coverWireBody(wirebody, assembly=None):
'''
Replaces a wirebody by a sheet.
- wirebody: a geometry.Sketch.
Should be a part of the project's geometry.
If it isn't, the sheet will be appended to the project.
'''
from empro import activeProject
covered = sheetFromWireBody(wirebody)
parts = activeProject.geometry()
assembly = (parts.pathToPart(wirebody) or (parts,))[-1]
try:
index = assembly.index(wirebody)
except ValueError:
assembly.append(covered)
else:
assembly[index] = covered
def _doCoverWireBody(selection=None):
'''
Function called when the menu item is clicked.
'''

Keysight EMPro Scripting Cookbook

101

Extending EMPro with Add-ons


from empro import geometry, gui
if not selection:
selection = gui.SelectionList.globalSelectionList().selection()
if not (len(selection) == 1 and
isinstance(selection[0], geometry.Sketch)):
gui.MessageBox.critical("Cover Wire Body",
"The Cover Wire Body add-on expects "
"exactly one Wire Body to be "
"selected.\n"
"Select one Wire Body and try again.",
gui.Ok, gui.Ok)
return
coverWireBody(selection[0])
def _onContextMenu(selection, selectedTypes):
'''
Filter for context menu to only allow sketches.
'''
from empro import geometry
from empro.toolkit import addon
if len(selection) == 1 and geometry.Sketch in selectedTypes:
return addon.makeContextAction("Cover Wire Body",
_doCoverWireBody,
icon=":/geometry/CoverWirebody.ico")
def _defineAddon():
from empro.toolkit import addon
return addon.AddonDefinition(
menuItem=addon.makeAction("&Cover Wire Body",
_doCoverWireBody,
icon=":/geometry/CoverWirebody.ico"),
onContextMenu=_onContextMenu
)

102

Keysight EMPro Scripting Cookbook

References
[1] ActiveState Python Recipes. frange(), a range function with float increments.
http://code.activestate.com/recipes/66472-frange-a-range-function-with-float-increments/.
[2] E. W. Dijkstra. Why numbering should start at zero. 1982.
http://www.cs.utexas.edu/~EWD/ewd08xx/EWD831.PDF.
[3] EIA/JESD59 - Bond Wire Modeling Standard. 1997.
http://www.jedec.org/sites/default/files/docs/jesd59.pdf.
[4] EMPro Documentation. Defining Parameters.
http://www.keysight.com/find/eesof-knowledgecenter.
[5] EMPro Documentation. Editing Bondwire Definition.
http://www.keysight.com/find/eesof-knowledgecenter.
[6] EMPro Documentation. Python Reference:
empro.mesh.PartGridParameters.
http://www.keysight.com/find/eesof-knowledgecenter.
[7] EMPro Documentation. Using Python Scripts.
http://www.keysight.com/find/eesof-knowledgecenter.
[8] Epydoc. Module metadata variables.
http://epydoc.sourceforge.net/epydoc.html#module-metadata-variables.
[9] Hoyt Koepke. 10 Reasons Python Rocks for Research (And a Few Reasons it
Doesnt).
http://www.stat.washington.edu/~hoytak/blog/whypython.html.
[10] B. Miller and D. Ranum. How to Think Like a Computer Scientist - List Slices.
http://interactivepython.org/courselib/static/thinkcspy/Lists/lists.html#list-slices.
[11] PEP 257 - Docstring Conventions.
http://www.python.org/dev/peps/pep-0257/.
[12] PEP 396 - Module Version Numbers.
http://www.python.org/dev/peps/pep-0396/.
[13] The Python Glossary. EAFP.
http://docs.python.org/2/glossary.html#term-eafp.
[14] The Python Glossary. Parameter.
http://docs.python.org/2/glossary.html#term-parameter.
[15] Python HOWTOs. Generator expressions and list comprehensions.
http : / / docs . python . org / 2 / howto / functional . html # generator - expressions - and - list comprehensions.
[16] The Python Language Reference. Boolean Operations and, or, pynot.
http://docs.python.org/release/2/library/stdtypes.html#boolean-operations-and-or-not.
[17] The Python Language Reference. Conditional Expressions.
http://docs.python.org/2/reference/expressions.html#conditional-expressions.
[18] The Python Language Reference. Special method names.
http://docs.python.org/2/reference/datamodel.html#special-method-names.
[19] Python Module of the Week. csv Comma-seperated value files.
http://pymotw.com/2/csv/index.html.
[20] The Python Standard Library. Mapping Types dict.
http://docs.python.org/2/library/stdtypes.html#mapping-types-dict.
[21] The Python Standard Library. csv - CSV File Reading and Writing.
http://docs.python.org/2/library/csv.html.
Keysight EMPro Scripting Cookbook

103

[22] The Python Standard Library. csv.writer.


http://docs.python.org/2/library/csv.html#csv.writer.
[23] The Python Standard Library. enumerate.
http://docs.python.org/2/library/functions.html#enumerate.
[24] The Python Standard Library. itertools Functions creating iterators for
efficient looping.
http://docs.python.org/2/library/itertools.html.
[25] The Python Standard Library. partial.
http://docs.python.org/2/library/functools.html#functools.partial.
[26] The Python Standard Library. Sequence Types str, unicode, list, tuple,
bytearray, buffer, xrange.
http://docs.python.org/2/library/stdtypes.html#sequence- types- str- unicode- list- tuplebytearray-buffer-xrange.
[27] The Python Tutorial. Dictionaries.
http://docs.python.org/2/tutorial/datastructures.html#dictionaries.
[28] The Python Tutorial. Keyword Arguments.
http://docs.python.org/2/tutorial/controlflow.html#keyword-arguments.
[29] The Python Tutorial. Unpacking Argument Lists.
http://docs.python.org/2/tutorial/controlflow.html#tut-unpacking-arguments.
[30] John W. Shipman. Duck typing, or: what is an interface?
http://infohost.nmt.edu/tcc/help/pubs/python/web/interface.html.
[31] stackoverflow. not None test in Python.
http://stackoverflow.com/questions/3965104/not-none-test-in-python/3965129#3965129.
[32] stackoverflow. Python tuple comparison.
http://stackoverflow.com/questions/5292303/python-tuple-comparison.
[33] stackoverflow. The Python yield keyword explained.
http://stackoverflow.com/questions/231767/the-python-yield-keyword-explained/231855#
231855.
[34] stackoverflow. Unzipping and the * operator.
http://stackoverflow.com/questions/5917522/unzipping-and-the-operator.
[35] Guido van Rossum. Why Python uses 0-based indexing.
https://plus.google.com/115212051037621986145/posts/YTUxbXYZyfi.
[36] Wavefront OBJ file format.
http://paulbourke.net/dataformats/obj/.
[37] wxPython Wiki. Passing Arguments to Callbacks.
http://wiki.wxpython.org/Passing%20Arguments%20to%20Callbacks.
[38] XKCD. Python.
http://www.xkcd.com/353/.

104

Keysight EMPro Scripting Cookbook

Index
Symbols
__author__, 96
__version__, 96
_defineAddon, 96

results, 71
topology, 76

dataset
toolkit, 72

DataSetMatrix, 67
dialog box, 97

A
abbreviation, 68
abs, 67
active ports, 58
add-ons, 95

AddonDefinition, 97
allAbbreviations, 68
allBondwires, 31
amplitudeMultiplier, 44
Arc, 35, 36
Assembly, 15

B
backend units, 70
Bondwire, 27

BondwireDefinition, 27
Boolean, 33
Box, 16

dimension, 65
dimensions, 65

78

exportDatasetsToCSV, 88
exportSurfaceSensorData, 76
exportToOBJ, 92
Expression, 16
ExpressionEdit, 97
Extrude, 22
extrudeFromWireBody, 22

toolkit, 74
as number, 66, 83
as sequence, 64
class, 63
complex numbers, 67
dimensions, 65
integrals, 86
matrices, 67, 75, 76

Keysight EMPro Scripting Cookbook

makeAction, 97
makeExponentialWaveguide,
32

makeLoft, 33
makePolygon, 19
makePolyline, 19
E
makeWaveguide, 46
maxPerFrequency, 84
enumerate, 22, 80, 90
menuItem, 97
enumerateFlat, 90
, 15
evaluateElectricFieldInPointModel
,

graphing

DataSet

display units, 70
docstrings, 96

far zone
graphs, 81, 83
CircuitComponent, 43
sensors, 52
CircuitComponentDefinition,
FBM, 15
44
Feature, 15
CITI files, 75
femFrequencyPlanList, 59
columnHeader, 71
findNearestIndex, 80
ComplexDataSet, 67
flatList, 30
context, 72
foiParameters, 59
context menu, 100
frequencies of interest, 59
conversionMultiplier, 68
FrequencyPlan, 59
conversionOffset, 68
fromReferenceUnits, 68, 97
Cover, 20, 21
coverAllWireBodies, 21, 100
coverWireBody, 100
G
crossSectionCircular, 36
getPortNumber, 58
csv, 87
getSMatrix, 67
CSV files, 51, 87
getWeightedResult, 83

line segment, 18
Loft, 32
logScale, 68

J
JEDEC, 28

multidimensional datasets, 66, 79

N
near field
results, 76, 78
sensors, 52
topology, 91

numberOfDimensions, 65

P
parameters
definition, 17
sweeping, 97
Part, 15

pathRectangular, 35
plotting, 74
polar graphs, 81
polygon, 19
polyline, 19
ports
internal, 43
Poynting vectors, 86
projectId, 72

R
recipe, 15
reduceDimensionsToIndex, 79
reduceDimensionsToNearestValue,
80
reference units, 68

replace_waveform, 50
ResultQuery, 71
RFID antenna, 35

L
Line, 18, 35

105

S
S-parameters, 58, 67, 75, 76

setFrequenciesOfIntereset,
59
Sheet Body, 20

sheetFromWireBody, 20, 100


showFarZoneGraph, 81
showPolarGraph, 81
showXYGraph, 74
SimpleDialog, 97
simulationId, 72
Sketch, 15, 18, 35
steady-state frequencies, 59
strUnit, 71
SurfaceSensor, 52
SweepPath, 35

T
timeIntegrate, 86
TimestepSampledWaveformShape,
49

topology, 76
toReferenceUnits, 68, 97
Touchstone files, 76
traversing geometry, 21

U
unitByAbbreviation, 68
unitByName, 68
unitClass, 68
User Defined Waveform, 48

V
Vector2d, 17
Vector3d, 17
VertexIndex, 76

W
waveforms, 50
waveguide ports, 45
Wire Body, 18, 100

X
XY graph, 74

106

Keysight EMPro Scripting Cookbook

Printed copies may not be current.


Check the Knowledge Center for the latest version.
www.keysight.com/find/eesof-empro-python-cookbook
Keysight EMPro Scripting Cookbook

107

You might also like