Empro Python Cookbook
Empro Python Cookbook
Empro Python Cookbook
Scripting Cookbook
to Serve Python
Users Guide
Notices
Keysight Technologies, Inc. 2014
Warranty
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.
Contents
1 Introduction
What this Cookbook is About
Learning Python
23
27
43
68
75
76
87
95
References
1
Introduction
What this Cookbook is About
Learning Python
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/
Introduction
Introduction
Introduction
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)
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.
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")
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)
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)
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"
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
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
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)
13
Introduction
Figure 1.4 Copying the getResult expression for a result available in the Result Browser
14
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
23
27
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
Extrude
Model
+recipe
1
Recipe
Cover
Box
0..*
Part
Sketch
0..*
Assembly
Feature
Loft
Pattern
Transform
=
=
=
=
empro.core.Expression("2 * 1 cm")
empro.core.Expression(3.14)
empro.core.Expression(42)
b * a / 36
NOTE
NOTE
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))
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
17
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
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)
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
19
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"
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)
21
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
22
23
24
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
25
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
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)
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
27
UI name
Description
empro.units.LENGTH
empro.units.SCALAR
Length
Proportional
empro.units.ANGLE
Angular
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
28
29
'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
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
30
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
31
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:
with
32
1
send
ln
length sbegin
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
33
34
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
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
# 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)
37
38
Description
INSCRIBED
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)
39
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
41
42
3
Defining Ports and Sensors
Creating an Internal Port
43
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
This creates a component called My Feed between two points and adds it to
the project.
NOTE
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.
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).
45
46
47
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))
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.
49
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.
# 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
51
53
54
4
Creating and Running Simulations
Setting Up the FDTD Grid
55
58
59
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.
55
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)
56
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
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
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))
59
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
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!"
61
62
5
Post-processing and Exporting
Simulation Results
An Incomplete Introduction To Datasets
Something About Units
63
68
71
Creating XY Graphs 74
Working with CITI Files
75
76
76
79
81
Multi-port Weighting 83
Maximum Gain, Maximum Field Strength 84
Integration over Time
86
87
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.
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
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.
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
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()
65
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"
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
67
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
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
sr
W/m/K
K
s
m/s
m**3
J/m**3
W/m**3
69
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
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))
71
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.
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
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
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.
73
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):
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])
74
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))
75
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')
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 "
77
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
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)
79
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)
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
81
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
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.
83
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
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
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)
85
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)]
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
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.")
87
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)
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
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
89
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]))
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
There are as many normals as vertices, and both lists contain (x, y, z) triplets.
Keysight EMPro Scripting Cookbook
91
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.
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)
93
94
6
Extending EMPro with Add-ons
Hello World!
95
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
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.
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
97
98
99
def _defineAddon():
from empro.toolkit import addon
return addon.AddonDefinition(
menuItem=addon.makeAction('Simple Parameter Sweep',
showParameterSweepDialog,
icon=":/application/ParameterSweep.ico")
)
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
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
101
102
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
104
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
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
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
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
107