Empro Python Cookbook PDF
Empro Python Cookbook PDF
Scripting Cookbook
to Serve Python
Users Guide
Notices
Keysight Technologies, Inc. 2014 Warranty Restricted Rights Legend
No part of this manual may be The material contained in this If software is for use in the performance
reproduced in any form or by any document is provided as is, and is of a U.S. Government prime contract
means (including electronic storage and subject to being changed, without or subcontract, Software is delivered
retrieval or translation into a foreign notice, in future editions. Further, to and licensed as Commercial computer
language) without prior agreement the maximum extent permitted by software as defined in DFAR 252.227-
and written consent from Keysight applicable law, Keysight disclaims all 7014 (June 1995), or as a commercial
Technologies, Inc. as governed by warranties, either express or implied, item as defined in FAR 2.101(a) or
United States and international with regard to this documentation as Restricted computer software as
copyright laws. and any information contained herein, defined in FAR 52.227-19 (June 1987)
including but not limited to the implied or any equivalent agency regulation
warranties of merchantability and or contract clause. Use, duplication
Edition fitness for a particular purpose. or disclosure of Software is subject
Keysight shall not be liable for errors to Keysight Technologies standard
Second edition, December 2014 or for incidental or consequential commercial license terms, and non-
Keysight Technologies, Inc. damages in connection with the DOD Departments and Agencies of the
1400 Fountaingrove Pkwy. furnishing, use, or performance of U.S. Government will receive no greater
Santa Rosa, CA 95403 USA this document or of any information than Restricted Rights as defined in
contained herein. Should Keysight FAR 52.227-19(c)(1-2) (June 1987).
and the user have a separate written U.S. Government users will receive no
agreement with warranty terms greater than Limited Rights as defined
covering the material in this document in FAR 52.227-14 (June 1987) or DFAR
that conflict with these terms, the 252.227-7015 (b)(2) (November 1995),
warranty terms in the separate as applicable in any technical data.
agreement shall control.
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.
1
Introduction
One of the popular features of EMPro is its extensive scripting interface using
the Python programming language. Users can automate many tasks, such as
creating and manipulating geometries, setting up and launching simulations,
processing results, and even extending the user interface with new controls.
The possibilities that the Python interface provides are endless. This cookbook
includes some of the code snippets written over the years in response to
questions from users on how to apply Python scripting in EMPro. Consider them
recipes of how to perform certain tasks. Some of them can be used verbatim,
while others will serve as a starting point for your own scripts.
The recipes are organized by topic in different chapters. Within each chapter,
they are roughly ordered from basic to advanced.
This cookbook is continuously being updated and more recipes are added.
Updated versions will be posted on the Keysight Knowledge Center.
www.keysight.com/find/eesof-empro-python-cookbook
Learning Python
5
1 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/
Python code can be executed in the EMPro user interface from the script editor,
Figure 1.1. Click on View > Scripting to open the editor. Create a new script, type
in your code and hit the run button. The scripts are embedded in the project
files so they are available wherever you open the same project. They can also
be exported and imported. See [7] for more information on how to use the script
editor.
Additionally, add-ons can be written that are loaded in the EMPro application
per user. This means you can use them to extend the user interface independent
EMPro also ships a commandline version of the Python interpreter. To use it, it is
however required that the correct environment variables are set to support the
EMPro runtime libraries. The easiest way to do that is to start it up is through the
emproenv or emproenv.bat shell scripts, as shown in Figure 1.2 and Figure 1.3.
c:\keysight\EMPro2012_09\win32_64\bin\emproenv.bat python
/usr/local/EMPro2012_09/linux_x86_64/bin/emproenv python
The recipes included in this cookbook are available as an EMPro Library. You can
drag-and-drop recipes from the library onto the Project Tree to add them to your
project. Then, you can open them in the Scripting editor to edit and run them.
Take the following actions to load the library in EMPro:
1 Download ScriptingCookbookLibrary.zip from the Keysight Knowledge Center.
www.keysight.com/find/eesof-empro-python-cookbook
2 Unarchive the downloaded file.
3 Open the Libraries window by clicking on View > Libraries.
4 In the Libraries window, click on File > Add Library..., select the unarchived
library and click Add.
A Guided Tour
Lets see how you can create a new project from scratch by just using Python
scripting. Start by ... creating a new project:
empro.activeProject.clear()
Creating Geometry
Substrate
The project is going to need some geometry. Read the introduction of Chapter 2
of the cookbook, page 9 and 10. Now, create box of 20 mm by 15 mm and 2 mm
high and add it to the project1 :
model = empro.geometry.Model()
model.recipe.append(empro.geometry.Box("20 mm", "2 mm", "15 mm"))
empro.activeProject.geometry().append(model)
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.
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
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")
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")
To alter a parts position, you manipulate its coordinateSystem. You cannot set
NOTE 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
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)
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.
OK, now your simulation is running and you have to wait for it to end. But how
do you do that programmatorically? Simple, you use the wait function! You pass
the simulation object youve just created and it will wait for its completion, or
failure.
from empro.toolkit.simulation import wait
empro.gui.activeProjectView().showScriptingWindow()
print "waiting ..."
wait(sim)
print "Done!"
How do you know if the simulation has succeeded? Simple, you check its status:
print sim.status
Post-processing
OK, you have now a completed simulation. How do you inspect the results?
Start with importing a few of the modules from the toolkit that you will need:
from empro.toolkit import portparam, dataset, graphing, citifile
If you have a simulation object like youve created before, you can grab the entire
S matrix, and plot it like this:
S = portparam.getSMatrix(sim)
graphing.showXYGraph(S)
If you dont have the simulation object, but you know the simulation ID, you can
use that as well. For example: portparam.getSMatrix(sim='000001'), or
simply portparam.getSMatrix(sim='1')
S is a matrix which uses the 1-based port numbers as indices. So you can also
plot individual S parameters:
graphing.showXYGraph(S[1, 2])
You can also 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)
Figure 1.4 Copying the getResult expression for a result available in the Result Browser
2
Creating and Manipulating Geometry
Creating new geometry is one of the more popular uses of EMPros scripting
facilities. Going from importing third party layouts or CAD, to creating
complex parametrized parts, this chapter is all about manipulating
empro.activeProject.geometry().
Any distinct object in the projects geometry is called a Part. As shown in
Figure 2.1, different kinds of parts exist such as Model, Sketch, and Assembly.
The last one, Assembly, is a container of other parts and as such the projects
geometry is a tree of parts. In fact, empro.activeProject.geometry() is just
an Assembly too, and will also be called the root part hereafter.
Model is the workhorse of the geometry modeling. It will be used for about any
part youll create, the notable exception being wire bodies for which Sketch is
used. A Model basically is a recipe of features: a flat list of operations that
describe how the model must be build. Extrude, Cover, Box, Loft are all
examples of such features. This way of constructing geometry is called
feature-based modeling (FBM).
In the recipes that follow in this chapter, many Model parts will be created and it
usually follows the following pattern:
1 A new model is created.
15
2 Creating and Manipulating Geometry
Extrude
+recipe Cover
Model Recipe
1
Box
0..*
Expressions
In the user interface of EMPro, almost anywhere where you can enter a quantity
(a length, a frequency, a resistance, ...), it is allowed to enter a full expression
with parameters, operators, functions and units [4]. The same expressions are
found on the scripting side as Expression objects. Thats why you see a lot
of functions accepting or returning expressions instead of floats as you might
have expected. You can construct them from a string (formulas), float, int or
another Expression object:
a = empro.core.Expression("2 * 1 cm")
b = empro.core.Expression(3.14)
c = empro.core.Expression(42)
d = b * a / 36
Vectors
Parameters
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()
Most geometrical modeling starts with a wire body. Sometimes theyll exist on
their own as thin wire models of dipole antennas (FDTD only), but usually theyre
needed as the profile of a sheet body, or as the cross section of an extrusion.
What is known as a Wire Body in the user interface, is known as a Sketch in the
Python API. It consists of a number of edges that must be added to the sketch.
Different kind of Edge elements exist such as Line, Arc, LawEdge, ...
Heres a very simple example that creates a single line segment. Start by
importing the geometry module so you dont need to type the empro prefix all of
the time. Then create a new Sketch and give it a name. A single Line edge is
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)
Adding adjacent line segments to the sketch will result in a polyline or, when
closed, a polygon. Recipe 2.1 shows two functions that can help with this. Both
take a series of vertices that need to be connected, saving you from needing
to duplicate the shared vertices of adjacent line segments. As an example, a
rectangle is created in the XY plane, centered around the origin.
makePolyline is a straight forward extension of the example above, connecting
all vertices. It takes two optional arguments sketch and name: by passing in
an existing Sketch, you can append to it instead of creating a new one.
Each line segment connects two vertices. An interesting way to extract the tails
and heads of individual line segments is demonstrated on line 12, using a bit of
slicing [10]. Suppose there are n vertices, then there are n 1 line segments.
vertices[:-1] yields all but the last of the vertices, and thus the n 1 required
segment tails. vertices[1:] yields all but the first of the vertices, and thus the
n1 required segment heads. zipping both gives us the n1 required (tail, head)
pairs.
makePolygon cunningly reuses makePolyline by observing that a polygon can
be created as a closed polyline, the first vertex being repeated as the last.
Because a single vertex cannot be concatenated to a list, the single-element
slice vertices[:1] is used instead.
None is great to use as default value for function parameters: if no argument for the
NOTE 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
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"
Sheet bodies are very simple to construct as they are basically covered
wire bodies. Recipe 2.2 demonstrates the straight forward function
sheetFromWireBody that accomplishes this task. It takes one argument
wirebody which is the Sketch to be covered. An optional name argument allows
to specify a name. As a prime example of the FBM techniques used in EMPro, it
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.
Suppose youve imported a PCB and all the traces are imported as wire bodies
so you only have their outlines. Thats a problem because you cannot simulate
them as such. First you must cover the outlines as sheet bodies to get the full
traces.
Recipe 2.3 demonstrates how to replace all wire bodies in the project by
equivalent sheet bodies. coverAllWireBodies is a simple recursive function
that iterates over all parts in an assembly. If it finds another Assembly, it simply
descends into it by calling coverAllWireBodies again with the new assembly
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.
height1 = 0.10
vertices1 = [
(-width1 / 2, -height1 / 2, 0),
(+width1 / 2, -height1 / 2, 0),
(+width1 / 2, +height1 / 2, 0),
(-width1 / 2, +height1 / 2, 0)
]
wirebody = makePolygon(vertices1)
extrude = extrudeFromWireBody(wirebody, name="my extrude")
empro.activeProject.geometry().append(extrude)
Creating traces is also similar to creating sheet bodies. Both start from a
Sketch, and both generate a single flat surface. But in case of a trace, the
Sketch represents the centerline rather than the outline. And if the centerline is
polygonal, you can reuse makePolyline of Creating Polylines and Polygons on
page 19 to create the Sketch.
Recipe 2.5 shows makeMeander that generates a meandering trace in the XY-
plane, as shown in Figure 2.2, so that the endpoints of the meander are (0, 0, 0)
and (length, 0, 0).
makeMeander starts by sanitizing the arguments by evaluating everything to
floating point values. Whatever the caller specifies as argumenta float, an
int, an Expression object or an expression as str, the function will continue
to work with the floating point representation. Before casting them to a float,
each argument is first converted to an Expression so that strings are correctly
interpreted as expressions. Attempting to directly cast a string to a float will fail
for expressions like "a * b" or "1 cm". For more information, see Parameters
on page 17.
The default value for pitch is twice the traceWidth, but you cant specify that
as such in the function definition. Instead, the default value for pitch is None,
and it is later replaced by twice the traceWidth. See note on page 19 for more
details on this technique.
Next up is calculating how many meanders can fit between start and endminus
the minimal leadsand how long each meander should be to achieve the desired
total length.
To create the actual centerline, a helper function x(k) will assist calculating
the x-coordinates of the vertices. In Python you can nest function inside other
functions as many times you want, and they see the variables of the surrounding
scope.
Each meander consists of two vertices with the same y-coordinate. Depending
on whether this is an even or odd meanderthe modulo operation k % 2 returns
the remainder of dividing k by 2the meander extends in the positive or negative
y -direction. The x-coordinate of the second vertex is equal to the first vertex of
the next meander.
if __name__ == "__main__":
meander = makeMeander(length="1 cm", totalLength="10 cm",
traceWidth=".5 mm", minimalLeadLength=".1 cm")
empro.activeProject.geometry().append(meander)
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
equation:
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.
# now you know everything, just add the equation and return.
model.recipe.append(geometry.Equation(x, y, z,
uMin, uMax,
vMin, vMax))
return model
EMPro supports a Bondwire feature that easily allows placing multiple bondwires
sharing a single profile definition. While the user interface has a nice dialog to
create the profile definitions, here it is demonstrated how you can do the same
using scripting.
An n-segment profile has n + 1 vertices. The first and the last vertex are the
begin- and endpoint of the individual bondwire, so youre left to define the n 1
vertices in the vertical plane in between [5]. Each vertex needs a pair of offsets
(t, z). Each offset can have a different reference and type (Table 2.1 and
Table 2.2).
addBondVertex has the task of adding individual vertices to the profile. Next to
the BondwireDefinition to be modified, it takes the six arguments required to
define a single vertex. The added bonus of the function is that it will check the
offset type and reference arguments for validity. checkValue compares their
value against the TYPES and REFERENCES tuples and raises a ValueError if they
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 = .
Flat Lists
In Recipe 2.2, recursion was used to traverse the part hierarchy. There is an
alternative approach if youre not interested in the exact part node within the
tree: flat part lists.
Any Assemblyand thus also empro.activeProject.geometry()has the
flatList method to request a single list of all parts in the assembly, including
the parts of its sub-assemblies (and their sub-assemblies, all the way down2 ). It
takes exactly one Boolean argument: whether or not to include the
sub-assemblies themselves in the list. You usually want to set it to False.
Load the QFN Package example. It consists of a number of assemblies and one
extrude. First, you simple iterate over the root assembly, and you print the type
and name of each part encountered:
for part in empro.activeProject.geometry():
print type(part), part.name
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 'empro.libpyempro.geometry.Assembly'> pcvia3
<type 'empro.libpyempro.geometry.Assembly'> cond2
<type 'empro.libpyempro.geometry.Assembly'> Bondwire
<type 'empro.libpyempro.geometry.Model'>
<type 'empro.libpyempro.geometry.Model'>
<type 'empro.libpyempro.geometry.Model'> Board
<type 'empro.libpyempro.geometry.Model'>
<type 'empro.libpyempro.geometry.Model'> bw1
...
2 Or up, depending how you look at it. Were used to draw part trees with the root node at the top and leaf
nodes at the bottom, but the nomenclature suggests otherwise. Well stick to the top-down representation.
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 'empro.libpyempro.geometry.Model'>
<type 'empro.libpyempro.geometry.Model'>
<type 'empro.libpyempro.geometry.Model'> Board
<type 'empro.libpyempro.geometry.Model'>
<type 'empro.libpyempro.geometry.Model'> bw1
<type 'empro.libpyempro.geometry.Model'>
<type 'empro.libpyempro.geometry.Model'>
<type '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 'empro.libpyempro.geometry.Model'> bw3
<type 'empro.libpyempro.geometry.Model'> bw4
<type 'empro.libpyempro.geometry.Model'> bw2
<type 'empro.libpyempro.geometry.Model'> bw1
if the part is a Model. In Python it is however more idiomatic to ask forgiveness rather than permission, or
EAFP [13].
try:
recipe = part.recipe
except AttributeError:
return False
return isinstance(recipe[0], empro.geometry.Bondwire)
assembly = assembly or empro.activeProject.geometry()
return [part for part in assembly.flatList(False)
if isBondwire(part)]
Simple rectangular waveguides are easily created by subtracting two boxes from
each other, one for the outside and one for the inside. But what if you want to
create something more complex, like an exponentially tapered waveguide as in
Figure 2.4? In Recipe 2.9, it is shown how you can use lofting to create a
piecewise linear approximation of such a waveguide.
with
1 send
= ln
length sbegin
startWidth = float(Expression(startWidth))
startHeight = float(Expression(startHeight))
endWidth = float(Expression(endWidth))
endHeight = float(Expression(endHeight))
length = float(Expression(length))
thickness = float(Expression(thickness))
steps = int(Expression(steps))
name = name or "waveguide"
Suppose you want to create a model of an RFID antenna like in Figure 2.5. If all
you want is a thin wire model, you can construct one using a wire body. But to
get a thick wire model, youll need to sweep a cross section profile along a path.
Recipe 2.10 will show you exactly how.
Rectangular Coils
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.
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).
Path sweeping expects the path to start in a point within a profile, for example its
NOTE 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.
thickness)
- Ly: outer spiral length along y-axis (not accounting for wire
thickness)
- pitch: spacing between turns (not accounting for wire thickness)
- bridgeLz: height of airbridge above coil
- feedLength: feedline length:
If positive, feeds sticks out of coil and length is measured
from outside (not accounting for wire thickness)
If negative, feeds is on inside of coile and length is measured
from inside (not accounting for wire thickness)
- feedOffset: distance from rightmost feed to right most side of
spiral (not accounting for wire thickness)
- feedSeparation: distance between leftmost and rightmost feed
(not accounting for wire thickness)
- name: name of coil, can be None
'''
from empro import core, geometry
# clean up arguments
numTurns = int(core.Expression(numTurns))
Lx = float(core.Expression(Lx))
Ly = float(core.Expression(Ly))
pitch = float(core.Expression(pitch))
bridgeLz = float(core.Expression(bridgeLz))
cornerRadius = float(core.Expression(cornerRadius))
feedLength = float(core.Expression(feedLength))
feedOffset = float(core.Expression(feedOffset))
feedSeparation = float(core.Expression(feedSeparation))
assert int(numTurns) == numTurns, "numTurns must be integer"
assert numTurns > 0
assert Lx > 0
assert Ly > 0
assert pitch > 0
if feedLength < 0:
feedLength -= (numTurns - 1) * pitch
halfLx, halfLy = Lx / 2, Ly / 2
numSectors = 4
path = geometry.Sketch()
# first feed
tail = start = geometry.Vector3d(0, 0, 0)
head = geometry.Vector3d(0, feedLength, 0)
path.add(geometry.Line(tail, head))
tail = head
# first feed is bridge?
if feedLength < 0:
z = -bridgeLz
head = tail + geometry.Vector3d(0, 0, z)
path.add(geometry.Line(tail, head))
tail = head
else:
z = 0
# to first corner
head = geometry.Vector3d(feedOffset + feedSeparation, feedLength, z)
tail = _side_with_corner(path, tail, head, cornerRadius,
numSectors - 1)
# the coil itself.
for k in range(numTurns):
for sector in range(numSectors):
dx = pitch * (k + (1 if sector >= 3 else 0))
dy = pitch * (k + (1 if sector >= 2 else 0))
sx, sy = _SECTORS[sector]
y = sy * (halfLy - dy) + halfLy + feedLength
isLastSide = (k == numTurns - 1) and \
(sector == numSectors - 1)
if isLastSide:
head = geometry.Vector3d(feedSeparation, y, z)
path.add(geometry.Line(tail, head))
tail = head
else:
x = sx * (halfLx - dx) - halfLx + \
feedOffset + feedSeparation
head = geometry.Vector3d(x, y, z)
tail = _side_with_corner(path, tail, head, cornerRadius,
sector)
# last feed is bridge?
if feedLength > 0:
z = bridgeLz
head = tail + geometry.Vector3d(0, 0, z)
path.add(geometry.Line(tail, head))
tail = head
# last feed
head = geometry.Vector3d(feedSeparation, 0, z)
path.add(geometry.Line(tail, head))
return path
def crossSectionCircular(radius=0.05e-3):
from empro import core, geometry
radius = float(core.Expression(radius))
assert radius > 0
sketch = geometry.Sketch()
fullCircle = True
sketch.add(geometry.Arc(
(radius,0,0),
(0,0,radius),
(-radius,0,0),
fullCircle
))
return sketch
Constant Description
INSCRIBED Use an inscribed polygon as approximation of the circle, the radius
remains unmodified. The resulting 3D wire will have a smaller volume
and smaller surface area.
EQUAL_AREA Use a polygon with the same area as the original circle. The resulting 3D
wire will have the correct volume, but a greater surface area.
EQUAL_PERIMETER Use a polygon with the same perimeter as the original circle. The
resulting 3D wire will have the correct surface area, but a smaller
volume.
fullCircle = False
path.add(Line(tail, cornerTail))
path.add(Arc(cornerTail, cornerMid, cornerHead, fullCircle))
return cornerHead
Recipe 2.11 shows two more examples of cross sections. Both are defined in the
XZ-plane, just like before. One is a simple rectangle, the other is a polygonal
approximation of a circle. Apart from the number of sides and the radius of the
circle to be approximated, the polygonal cross section also takes an argument
that controls how the circle must be approximated: preserving the radius, area
or perimeter. Therefore a number of constants is defined on line 16, their exact
meaning is explained in Table 2.3.
APPROXIMATION_POLICIES = (INSCRIBED,
EQUAL_AREA,
EQUAL_PERIMETER) = range(3)
def crossSectionPolygonal(radius=0.05e-3, numSides=6,
approximationPolicy=INSCRIBED):
from empro import core, geometry
import math
radius = float(core.Expression(radius))
numSides = int(core.Expression(numSides))
assert radius > 0
assert numSides >= 3
assert approximationPolicy in APPROXIMATION_POLICIES
pie_angle = 2 * math.pi / numSides
if approximationPolicy == EQUAL_AREA:
area = math.pi * (radius * radius)
poly_radius = math.sqrt(2 * area /
(numSides * math.sin(pie_angle)))
elif approximationPolicy == EQUAL_PERIMETER:
perimeter = 2 * math.pi * radius
poly_radius = perimeter / (2 * numSides *
math.sin(pie_angle / 2))
else:
poly_radius = radius
if numSides % 4 == 0:
theta_offset = pie_angle / 2
else:
theta_offset = 0
def vertex(k):
theta = k * pie_angle + theta_offset
return (poly_radius * math.cos(theta),
0,
poly_radius * math.sin(theta))
sketch = geometry.Sketch()
for k in range(numSides):
tail, head = vertex(k), vertex((k + 1) % numSides)
sketch.add(geometry.Line(tail, head))
return sketch
Spiral Coil
Finally, Recipe 2.12 also presents you a function that generates a path for a
circular RFID coil. The function parameters are similar to pathRectangular, but
Lx and Ly are replaced by a single diameter parameter, and feedOffset is no
longer used. A new parameter is discretisationAngle: unless zero, it is used
to approximate the spiral by linear segments, which gives a more predictable
sweeping behavior.
# spiral
xy = lambda theta: (math.sin(theta) * rho(theta) + xOffset,
-math.cos(theta) * rho(theta) + yOffset)
xEnd, yEnd = xy(thetaEnd)
if not discretisationAngle:
# use a true spiral wire.
# doesn't really work wel for the sweep though.
center = (xOffset, yOffset, z)
sketch.add(Spiral(center, tail, -pitch,
thetaEnd - thetaStart, Spiral.Right))
tail = (xEnd, yEnd, z)
else:
n = int(math.ceil((thetaEnd - thetaStart) /
discretisationAngle))
for k in range(1, n):
theta = thetaStart + k * discretisationAngle
x, y = xy(theta)
head = (x, y, z)
sketch.add(geometry.Line(tail, head))
tail = head
head = (xEnd, yEnd, z)
sketch.add(geometry.Line(tail, head))
tail = head
# last feed is bridge?
if feedLength > 0:
z = bridgeLz
head = (xEnd, yEnd, z)
sketch.add(geometry.Line(tail, head))
tail = head
# last feed
head = (xEnd, 0, z)
sketch.add(geometry.Line(tail, head))
tail = head
return sketch
3
Defining Ports and Sensors
Recipe 3.1 shows a full example of how to add an internal port. The following
sections will explain it in more detail.
Circuit Components
Internal ports are defined by impedance lines between two endpoints, together
with some properties. To create one, a CircuitComponent with a proper
CircuitComponentDefinition must be inserted in the project. Heres a
minimal example of what to do:
feed = empro.components.CircuitComponent()
feed.name = "My Feed"
feed.definition = empro.activeProject.defaultFeed()
feed.tail = (0, 0, "-1 mm")
feed.head = (0, 0, "+1 mm")
empro.activeProject.circuitComponents().append(feed)
43
3 Defining Ports and Sensors
This creates a component called My Feed between two points and adds it to
the project.
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
Oddly enough, the Automatic and Step waveform shapes are not exported to
NOTE 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).
feed = empro.components.CircuitComponent()
feed.name = "Feed"
feed.definition = feedDef
feed.port = True
feed.direction = "Automatic" # for FDTD
feed.polarity = "Positive"
feed.tail = (0, 0, "-1 mm")
feed.head = (0, 0, "+1 mm")
empro.activeProject.circuitComponents().append(feed)
Creating waveguide ports from scripting seems a bit tricky at first. You need to
attach the waveguide to a face of a geometrical part, and you need a face ID for
that. Picking tools exist to give this information if you allow for user interaction.
But if you want to do it fully automatically, knowing the right face ID is difficult.
The trick around it is to create auxiliary geometry that has only one face which
ID is known. The prime candidate for this task is a sheet object (or Cover), and
You have imported a design with a lot of internal ports, but they are all defined
as simple impedance lines. You would like to change them into rectangular
sheet ports of a certain width. setRectangularSheetPort of Recipe 3.3 helps
to modify a circuit component to use the rectangular sheet extent. You pass it
the component to be modified and the desired width of the sheet.
To define a sheet port automatically, theres one tricky problem to solve: in what
geometrical plane does the sheet need to be defined? There are an infinite
number of planes passing through the impedance line, and you need to pick the
best one. The best orientation is not uniquely defined however, and therefore
setRectangularSheetPort accepts a third optional argument: the zenith
vector. The sheet extension will be oriented as orthogonal as possible to the
zenith, while still going through the impedance line.
To define a sheet extent, you need to set two corner points of the quadrilateral:
one for the tail and one for the head. The other two corner points are implicitly
defined by mirroring the first two across tail and head. You need an offset vector
orthogonal to both the impedance line and zenith vector, so you take their cross
product. The offsets size should be half the sheets width, so you scale
accordingly.
If the impedance line is parallel to the zenith vector, then their cross product is
the null vector and no proper offset can be determined. When using the default
zenith vector (0, 0, 1), this will happen when applying this recipe to vertical ports.
Using a vertical zenith vector causes the sheet extents to be as horizontal as
possible, and this is impossible to do in case of vertical ports. As a result,
setRectangularSheetPort will fail with an error message, and you should
override the zenith vector with the normal vector of the plane in which you want
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.
When using user defined waveforms like this, one must keep the following in mind:
NOTE
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.
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.
def replace_waveform(waveform):
'''
Searches the project for a waveform with the same name.
If it can be found, it's replaced by by a copy of the new one.
Otherwise, the copy of the new waveform is simply appended.
Finally, it returns a reference to the new copy as owned by the
project
'''
import empro
waveforms = empro.activeProject.waveforms()
index = waveforms.index(waveform.name)
if index < 0:
waveforms.append(waveform)
return waveforms[len(waveforms)-1]
else:
waveforms.replace(index, waveform)
return waveforms[index]
When designing antennas, youll want to setup a far zone sensor in your product
to calculate the antenna gain. This simply requires adding a FarZoneSensor
instance to activeProject.farZoneSensors(). Recipe 3.5 shows how to add
a spherical far zone sensor that collects steady state data. The properties of the
sensor object map directly onto the properties you can find in the user
interface, so this should be straight forward to use. The possible values for
coordinateSystemType are ThetaPhi, AlphaEpsilon and ElevationAzimuth,
which dictates the meaning of the first and second angle properties. When using
a constant angle, only the start value should be set.
Adding near field sensors is slightly more complicated than far zone sensors,
as you need to create a separate geometry and data definitionwhich you can
reuse. The following example illustrates how you can add a planar near field
sensor collecting steady state electric field data.
PlaneSurfaceGeometry defines a plane using a position of a point on the
plane, and a normal vector. The properties of SurfaceSensorDataDefinition
map directly onto the properties available in the user interface. You finally create
a SurfaceSensor, set both geometry and definition, and you add it to
4
Creating and Running Simulations
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.
In the user interface, theres a Basic and Advanced mode for setting the grid
size. In scripting, theres only the advanced mode. Most of the cellSizes
settings take a tuple of three values: for the X, Y and Z directions. Theres the
target and minimum cell sizes to be set, and both accept expressions. So you
can use a tuple of strings, floats, Expression objects, or a mixture of these as
shown in the example below. You can also use Vector3d objects if you
wantsee Vectors on page 17. minimumType corresponds to the Ratio check
box: "RatioType" corresponds with the selected state, "AbsoluteType" makes
the minimum absolute and thus corresponds with the cleared state of the check
box.
grid = empro.activeProject.gridGenerator()
grid.cellSizes.target = ("1.0 mm", 1e-3, "1.0 mm")
grid.cellSizes.minimum = ("0.1 mm", 1e-4, 0.1)
grid.cellSizes.minimumType = ("AbsoluteType", "AbsoluteType",
55
4 Creating and Running Simulations
"RatioType")
For the padding, you can either set the number of padding cells using the
padding attribute, or you can directly set the bounding box using the
boundingBox attribute. Both have a lower and upper attribute accepting triples
of expressionsas demonstrated in various ways below. You must however be
careful to set the gridSpecificationType to the method youve chosen,
similar to the radio buttons in the user interface:
grid.gridSpecificationType = "PaddingSizeType"
grid.padding.lower = (15, "15", "10 + 5")
grid.padding.upper = (15, 15, 0) # no padding in lower Z direction
or:
grid.gridSpecificationType = "BoundingBoxSizeType"
grid.boundingBox.lower = (-0.015, "-15 mm", "-10 mm - 5 mm")
grid.boundingBox.upper = (0.015, "15 mm", 0)
Adding a fixed point involves creating a FixedPoint, setting its location and
specifying for which axes it will fix the grid. The axes attribute is a string that
represents a bit field corresponding to the state of the Fixed check boxes. Its
a combination of three flags "X", "Y" and "Z" that can be combined using the
pipe as OR operator. Finally, you add the fixed point to the grid generator.
def addFixedPoint(location, axes="X|Y|Z"):
point = empro.mesh.FixedPoint()
point.location = location
point.axes = axes
empro.activeProject.gridGenerator().addManualFixedPoint(point)
addFixedPoint(("1 mm", "2 mm", 0 ), "X|Y") # for X and Y only.
region.cellSizes.minimumType = ("AbsoluteType",
"AbsoluteType",
"AbsoluteType")
region.gridRegionDirections = "X|Y|Z"
region.regionBounds.lower = ("-5 mm", "-5 mm", "-5 mm")
region.regionBounds.upper = ("5 mm", "5 mm", 0)
grid.addManualGridRegion(region)
In the Limits tab, you can set the Maximum Cell Step Factor and the Maximum
Cells, which correspond to the following properties. The state of the check box is
represented by useMaximumNumberOfCells.
grid = empro.activeProject.gridGenerator()
grid.maximumStepFactor = 2
grid.useMaximumNumberOfCells = True
grid.maximumNumberOfCells = "10 million"
Assuming you know what youre doing, you can further refine the grid
settings by having individual objects adding fixed points and grid regions
automaticallybesides the one youve added manually like shown above.
Any part of the projects geometry has a gridParameters attribute that can be
used to further refine the FDTD grid. To have the object to automatically add
fixed points, you first need to set the useFixedPoints attribute. Then specify
fixedPointsLocations and fixedPointsOptions which are again a
combination of string constants that can be ORed. Check the reference
documentation [6] to know what the valid constants are.
Heres a function that will set these parameters for all objects at once. Not all
parts have grid parameters though, in which case part.gridParameters will
evaluate to None and attempting to set any of its attributes will result in an
AttributeError being raised. To prevent the function from failing in such case,
the try / except clause is added to catch and ignore the exception:
def SetAutomaticFixedPointsForAllObjects(locations="All",
options="C1VertexDiscontinuities|GridAxisAlignedLineEndPoints"):
for part in empro.activeProject.geometry().flatList(True):
try:
part.gridParameters.useFixedPoints = True
part.gridParameters.fixedPointsLocations = locations
part.gridParameters.fixedPointsOptions = options
except AttributeError:
pass
By setting useGridRegions you can also have the part to include a grid region
automatically. Specify cellSizes as above. The boundaryExtensions can
be used to expand the region beyond the parts bounding box. Set it in normal
bounding box fashion.
The short story to create an FDTD simulation is that you grab the
empro.activeProject.createSimulationData() object to specify the
simulation settings of the simulations to be created, you set its engine to FDTD,
and you create a new simulation with createSimulation.
simSetup = empro.activeProject.createSimulationData()
simSetup.engine = "FdtdEngine"
# ...
sim = empro.activeProject.createSimulation(True)
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.
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))
Just like for FDTD simulations, the short story is to grab the
createSimulationData object, set the engine to FEM, specify the other
settings and call createSimulation:
simSetup = empro.activeProject.createSimulationData()
simSetup.engine = "FemEngine"
# ...
sim = empro.activeProject.createSimulation(True)
based indexing. There are two major conventions for index ranges: zero-based half-open intervals, and one-
based closed intervals. As most general purpose programming languages, Python uses the former as it turns
out to be the practical choice for most applicationsregrettably matrix calculation is not one of them. As a
consequence, the upper bound of range is excluded so that range(len(some_list)) covers all
valid indices for that list, and that len(range(n)) == n. [2, 35]
freqPlans = simSetup.femFrequencyPlanList()
Now you know how to create and run simulations, but maybe you also want to
add some code to process the results (see Chapter 5, Waiting Until a Simulation
Is Completed). Then youll need to be able to wait until a simulation is
completed. You can write a simple loop that checks the status of the
Simulation object (which is simply the return value of createSimulation)
until its completed. As with any good polling loop, you put in a short sleep. And
in EMPro you also want to put in a processEvents call so that the user interface
stays responsive:
import empro, time
sim = empro.activeProject.createSimulation(True)
while sim.status in ('Queued', 'Running', 'PostProcessing',
'Interrupting', 'Killing'):
time.sleep(.1)
empro.gui.processEvents()
To make things easier, the simulation module in the toolkit has a function wait
that nicely wraps it up for you. You pass it a Simulation object and it will wait
until that simulation is completed.
import empro, empro.toolkit.simulation
sim = empro.activeProject.createSimulation(True)
empro.toolkit.simulation.wait(sim)
print "Done!"
You can also wait for more than one simulation in a single call by passing the
simulation objects in a list:
import empro, empro.toolkit.simulation
sim1 = empro.activeProject.createSimulation(True)
# ...
sim2 = empro.activeProject.createSimulation(True)
empro.toolkit.simulation.wait([sim1, sim2])
print "Both sim1 and sim2 are done!"
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!"
5
Post-processing and Exporting
Simulation Results
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.
63
5 Post-processing and Exporting Simulation Results
That sort of wraps it up. Its the alphabut not the omegaof multidimensional
data representation in EMPro. It can be one-dimensional like the time series of
the current through a circuit component. It can also be multidimensional like
far zone data, depending on two angles and the frequency. Most datasets are
discrete; they are sampled for certain values of their dimensions. There are also
continuous datasets, but these are rare.
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.
Discrete datasets are just arrays of floating point numbers. So its only natural to
give them a similar interface like Pythons tuple or list [26]. That means you
can get their size with the len operator, and index values with the [] operators1 :
s11 = empro.toolkit.dataset.getResult(sim=1, run=1, object='Feed',
result='SParameters',
complexPart='ComplexMagnitude')
f = file("s11.txt", "w")
for k in range(len(s11)):
f.write("%s\n" % s11[k])
f.close()
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].
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()
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) . . .
Datasets also behave a lot like normal Python numbers. That means you can
add, subtract, multiply or divide datasets just like they are simple numbers. The
operations are then performed on the individual elements. The requirement for
this to work is that both operands have the same dimensions and are of the
same size.
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
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
In this chapter, youll see a lot of unit handling code. So its best to have an
introductory section on units.
Unit Class
The term unit class is used to indicate physical quantities like time, length,
electric field strength, ... All datasets have an attribute unitClass that will tell
you want kind of quantity the dataset contains. The value of this attribute is a
string like "TIME", "LENGTH" or "ELECTRIC_FIELD_STRENGTH". Unitless data is
specified using the "SCALAR" unit class.
The full list of known unit classes is available in the documentation, or can be
printed as follows:
for unitClass in sorted(empro.units.displayUnits()):
print unitClass
For your convenience, all these strings are also defined as constants in the
units module: empro.units.TIME, empro.units.LENGTH,
empro.units.ELECTRIC_FIELD_STRENGTH, empro.units.SCALAR, ...
Reference Units
For each unit class, theres an assumed reference unit that is used as an
absolute standard when converting physical quantities from one unit to another.
These reference units simply are the SI units, expanded with directly derived
2
ones like = ms2 A
kg
. 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
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)
When creating your own DataSet objects, its best to populate them with data in
NOTE 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
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))
To postprocess results, you first need to get them. getResult is your first tool of
the trade here. You probably want to read this section a few times to make sure
you fully understand this.
Fundamentally, retrieving data from simulations requires painstakingly filling in
11 attributes of a ResultQuery, in order! Only then you can retrieve the dataset.
You also should not forget to add the project to the result browser first,
otherwise no data will be available.
For example, to get the current through Port1 of the first run of the FDTD
simulation of the Microstrip Low Pass Filter example, you would need to do all
of this:
empro.output.resultBrowser().addProject(
"C:/keysight/EMPro2012_09/examples/ExampleProjects/"\
"%Microstrip#20%Low#20%Pass#20%Filter")
query = empro.output.ResultQuery()
query.projectId = "C:/keysight/EMPro2012_09/examples/ExampleProjects/" \
"%Microstrip#20%Low#20%Pass#20%Filter"
query.simulationId = '000001'
query.runId = 'Run0001'
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')
In the user interface, for any result listed in the Results Browser, you can retrieve
NOTE the corresponding getResult call by clicking Copy Python Expression
in the context menu. Then you simply paste the expression in your script.
You see almost a one-on-one relationship with the query fields. Thats because
getResult will exactly build such a query for you! Only projectId is missing,
but thats handled by context.
getResult starts with a context in which it needs to interpret the arguments
that follow. It can be many things:
The project path as a string, as in the example above. This tells getResult
to add the project to the result browser if necessary, and to use that string as
the projectId.
A Simulation object retrieved from createSimulation. This not only tells
the projectId, but also already fills in the simulationId. So, when using
this as a context, its no longer necessary to specify the sim parameter2 .
from empro.toolkit import simulation, dataset
sim = empro.activeProject.createSimulation(True)
simulation.wait(sim)
current = dataset.getResult(sim, run=1, object='Port1', result='I')
None. when omitted, the active project is assumed as the context, and the
projectId is set to its rootDir.
Once you have to context, you still need to specify exactly what result you want.
The various parameters you can specify are:
2 Using a Simulation object as context does not work in EMPro 2012.09 for OpenAccess projects.
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 S-
parameters. 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.
Thats a lot of parameters and a lot of possible arguments, but what you need to
NOTE 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.
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):
By default, the graph axes use display units according to the unit class of the
dataset (for the Y axis) and its dimension (for the X axis). If you want to override
that, you can specify the units abbreviation as following:
graphing.showXYGraph(v, xUnit="ps")
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])
The graph title can be set using the title parameter, the axis labels with
xLabel and yLabel. If you want to use custom labels in the legend, you can
supply a list of strings to the names parameter, one string per dataset. Heres an
example using the Microstrip Low Pass Filter example:
s11_fdtd = dataset.getResult(sim=1, run=1, object='Port1',
result='SParameters')
s11_fem = dataset.getResult(sim=2, run=1, object='Port1',
result='SParameters')
graphing.showXYGraph(s11_fdtd, s11_fem, title="FDTD vs FEM",
names=["S11 (FDTD)", "S11 (FEM)"],
xLabel="f", yLabel="S")
You can zoom in on the graph by setting xBounds and yBounds to the area of
interest. These parameters expect pairs of floats in reference units. That means
expressions must be evaluated first. For S-parameters, that means you need to
provide limits in linear scale. So say 1 instead of 0 dB:
graphing.showXYGraph(S, xBounds=(0, 10e9),
yBounds=(float(empro.core.Expression("-50 dBa")),
1))
The toolkit contains a citifile module that can be used to read and write CITI
files. It has a class CitiFile that behaves a lot like a ordinary dict, and a
couple of helper functions.
Exporting S-parameters
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
The asMatrix method recognizes patterns in key names like "S[1,1]" and
groups them into one matrix:
S = citi.asMatrix('S')
You have setup a surface sensor in EMPro, and you want to export the data to a
text file. These sensors already have sampled the data in discrete vertices
(x, y, z) and their DataSets usually have two dimensions: a Frequency or
Time dimension, and VertexIndex. The latter is a dimension of indices over a
list of vertices, which can be retrieved from the topology attribute.
Recipe 5.1 shows a function exportSurfaceSensorData that can export
steady-state data of surface sensors to a tabulated text file. It writes one line per
vertex. The first three columns contain the x, y and z coordinate values. They
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.
"empro.toolkit.dataset.getResult")
vertices = topology.vertices
# units for formatting.
freqUnit = empro.units.displayUnits()[units.FREQUENCY]
lengthUnit = empro.units.displayUnits()[units.LENGTH]
dataUnit = empro.units.displayUnits()[dataset.getUnitClass()]
# open file, and write info about datatype.
out = open(path, 'w')
out.write('# %s\n' % columnHeader(dataset.name, dataUnit))
# write column info
line = [columnHeader(x, lengthUnit) for x in ('X', 'Y', 'Z')]
for freq in frequencies:
line += [
"Re,%s %s" % (strUnit(freq, freqUnit),
freqUnit.abbreviation()),
"Im,%s %s" % (strUnit(freq, freqUnit),
freqUnit.abbreviation()),
]
out.write('# %s\n' % '\t'.join(line))
# for each point, write xyz triple +
# real/imag pairs for each frequency.
for (k, vertexIndex) in enumerate(vertexIndices):
vertex = vertices[int(vertexIndex)]
line = [strUnit(x, lengthUnit) for x in vertex]
for i in range(len(frequencies)):
x = dataset.at(i, k)
line += [strUnit(x.real, dataUnit),
strUnit(x.imag, dataUnit)]
out.write('\t'.join(line) + '\n')
In FEM, near field sensors dont sample on a regular grid like in FDTD, and its
not limited to discrete grid locations either. So, depending on your needs, it may
be more convenient to bring-your-own sample points instead, to directly
evaluate the FEM near fields.
The fem module of the toolkit provides a class NearField that provides the
necessary interface for doing so. In Recipe 5.2 it is shown how to use that to
evaluate the electric field in a single point for all available frequencies.
evaluateElectricFieldInPoint accepts an initialized NearField object and
a 3D coordinate. It iterates over all frequencies and evaluates the electric field.
This results in a triple of complex valuesone for each of the X-, Y- and
Z-componentsand since a real-valued dataset will be returned, the vector
magnitude needs to be computed. When doing so, make sure not to sum the
When dealing with multidimensional datasets like far zone data, you sometimes
want to reduce the number of dimensions by fixating some of them to a certain
value. For example, most far zone field results have three dimensions:
Frequency, Theta and Phi. But XY-plots must be one dimensional: you can
plot the field strength in function of frequency but only for a fixed theta and phi,
you can plot a cross section for all theta if you fix phi and frequency to one value.
Load the EMI Analysis example to get the Far Zone Sensor 3D dataset. To
plot it using showXYGraph you need to reduce the three-dimensional dataset
to a one-dimensional one. You can do that with reduceDimensionsToIndex of
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".
When creating XY or polar graphs, youll find that the graph functions only
accept one-dimensional datasets. Multi-dimensional datasets like far zone fields
are not accepted. In order to plot such data, some work needs to be done to
reduce them to one-dimensional datasets. Recipe 5.4 shows you how you can
accomplish that.
The first part of showFarZoneGraph basically checks if a valid dataset is being
supplied. All steady-state far field datasets have three dimensions: the first is
Frequency and the other two are angular (for example Theta and Phi). The
function is however a bit more liberal than that and also accepts datasets with
one or two dimensions3 . This can occur when you already have reduced a three-
dimensional far zone dataset to fix one or both of the angles to a specific value.
If only the frequency dimension is present, showFarZoneGraph falls back to a
regular data versus frequency XY plot, see line 28.
Next, on line 34, it searches which of the angleDims dimensions are single
valued. It builds a dictionary indices that maps their names to zero, the only
index possible within a single valued dimension. Later on, this dictionary will be
used to reduce the dataset to get rid of these dimensions. Again, a dict
comprehension is used with a condition: only dimensions with one value will get
an entry.
If indices and angleDims are of the same size, then all angular dimensions are
single valued, and you again have a data versus frequency plot. Use indices
to reduce the dataset to one dimension and show a regular XY graph. The **-
operator is used to unpack indices as separate keyword arguments [29], since
reduceDimensionsToIndex expects them so.
If the size of indices and angleDims differs more than one, it means that at
least two angular dimensions have more than one value4 . This kind of datasets
cannot be plotted using polar graphs and require a 3D plot. An error is raised.
3 If the dataset has only one dimensions, it must be Frequency. If it has two dimensions, the first must be
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.
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.
Whenever you weight vector field data, make sure you weight the
NOTE 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.
Youve computed far zone fields all around your antenna, but youre only
interested in the maximum gain per frequency. You can find this number in the
3D plots of the far field, but how do you get in Python?
maxPerFrequency in Recipe 5.6 will help with that task. It takes advantage of
the fact that you can call the max operator on any dataset to retrieve its
maximum value:
gain = dataset.getResult(sim=2, object='3D Far Field',
timeDependence='SteadyState',
result='Gain')
print max(gain)
But since you want to know the maximum value per frequency, youll first have
to reduce the dataset to each frequency. Doing that with a list comprehension
results in:
freq = gain.dimension(0)
print [max(dataset.reduceDimensionsToIndex(gain, Frequency=index))
for index,f in enumerate(freq)]
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)
return makeDataSet(maxValues,
dimensions=[freq.clone()],
name="Max(%s)" % ds.name,
unitClass=ds.unitClass)
Suppose you have a time dependent DataSet, and you want to integrate it. It
could be a one-dimensional signal like instantaneous power, or maybe a
two-dimensional signal like the transient Poynting vectors on a surface sensor.
Recipe 5.7 shows you timeIntegrate that helps with this task. Applying it to a
one-dimensional transient signal, simply returns a number:
p = empro.toolkit.dataset.getResult(sim=1, run=1, object='Port1',
result='InstantaneousPower')
print timeIntegrate(p)
Integrating Poynting vectors will reduce the dataset dimensionality from two to
one, eliminating Time and leaving VertexIndex. However, if youre integrating
vector data, you must be carefull to integrate the components seperately. Failing
to do so will result in integrating the vector magnitude instead, which will yield
the wrong result:
sx, sy, sz = [dataset.empro.toolkit.getResult(sim=1, run=1,
object='Surface Sensor',
result='S',
component=comp)
for comp in ('X', 'Y', 'Z')]
Sx, Sy, Sz = [timeIntegrate(s) for s in (sx, sy, sz)]
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.
What about a general function that can export arbitrary datasets of arbitrary
dimensions to a general format? The Python Standard Library contains a module
csv to work with comma-separated values (CSV) files [21, 19]. Recipe 5.8 shows
an function that uses that module to export any number of datasets to a CSV
file. So you can export just one dataset, or ten, or a whole S-matrix. They can
be real, complex, or a mixture of both. They can be of different unit classes, so
you can mix voltage and current data.
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')
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
Whats still left is creating the actual CSV file. On line 17, a writer is initialized
with the excess keyword arguments **kwargs.
8 Well, thats technically not true, CSV data can be anything you want, but limiting yourself to floating point
value will make it a lot easier to handle the data in external programs.
9 If you think of matrices as lists of lists, then this is basically transposing the matrix, and this idiom is often
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.
keys = sorted(ds.keys())
subsets = [ds[key] for key in keys]
if ds.isDiagonal():
subnames = ["%s[%s]" % (name, r) for (r, c) in keys]
else:
subnames = ["%s[%s,%s]" % (name, r, c)
for (r, c) in keys]
unwrapped += unwrapDatasets(subsets, subnames)
else:
try:
re, im = ds.real, ds.imag
except AttributeError:
unwrapped.append((ds, name))
else:
unwrapped += [
(re, "Re(%s)" % name),
(im, "Im(%s)" % name),
]
return unwrapped
def enumerateFlat(dimensions):
from itertools import product
return enumerate(product(*dimensions))
def encodeUtf8(x):
if isinstance(x, unicode):
return x.encode("utf8")
return x
Youve exported the near field data of a surface sensor so that you can visualize
it in another tool, but you also need to accompanying surface geometry.
Wavefront OBJ [36] is a simple 3D geometry definition file format and widely
supported, which makes it ideal for this purpose.
There are as many normals as vertices, and both lists contain (x, y, z) triplets.
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 -1 -1 -1
v -1 -1 +1
v -1 +1 -1
v -1 +1 +1
v +1 -1 -1
v +1 -1 +1
v +1 +1 -1
v +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 -1 0 0
vn +1 0 0
vn 0 -1 0
vn 0 +1 0
vn 0 0 -1
vn 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 1 2 4 3
f 3 4 8 7
f 7 8 6 5
f 5 6 2 1
f 1 3 7 5
f 2 6 8 4
If each facet vertex also has a normal, you write it next to the vertex index,
separated with a double slash:
f 1//1 2//5 4//1 3//1
f 3//4 4//4 8//4 7//4
f 7//2 8//2 6//2 5//2
f 5//3 6//3 2//3 1//3
f 1//5 3//5 7//5 5//5
f 2//6 6//6 8//6 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.
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))
6
Extending EMPro with Add-ons
Hello World! 95
Adding Dialog Boxes: Simple Parameter Sweep 97
Extending Context Menu of Project Tree: Cover Wire Body 100
Since 2012, EMPro provides an add-on mechanism that allows you to easily
extend the GUI with new functionality that can be written or customized by
yourself. Before, one had to copy/paste or import Python scripts in every project
you wanted to use it, select the right script in the Scripting editor, and press
play. With the new add-on mechanism, it becomes possible to insert new
persistent commands in the Tools menu or in the Project Trees context menu.
Given the nature of this cookbook, it should come as no surprise that these
add-ons must be written as Python modules. In this chapter, it is shown how to
create one.
The Keysight Knowledge Center has a download section where you can find
additional add-ons. Take a look at their source code to see how they work, it
may help to build your own add-on. Or take an existing one, and modify it for
your own purposes. When youve created an add-on that you think may be
useful for others, you can submit it on the knowledge center so that we may
make it available as a user contributed add-on.
www.keysight.com/find/eesof-empro-addons
Hello World!
To get your feet wet, this chapter starts with the Hello World of the add-ons, to
demonstrate the basic elements every add-on should have. Recipe 6.1 shows a
minimal implementation that will add a new command Tools > Hello World
showing a simple message.
The meat and mead of this example add-on is the function helloWorld defined
on lines 8 to 10. Calling this function will cause a message box to appear saying
Hi There ... In a real world case, you would of course have something more
usefull instead.
95
6 Extending EMPro with Add-ons
The helloWorld function by itself would already make a nice Python module,
but its not an add-on yet. The missing elements are shown one by one.
Documentation
To document add-ons, you simply use docstrings [11] which are Pythons natural
mechanism to document modules, classes, or functions:
'''
Documenting our Hello World Add-on
'''
To add one, simply write a string literal as the first statement in your module.
Here, the documentation is a triple quoted blockstring on lines 1 to 3. Although
normal string literals will do just fine, most docstrings will be blockstrings
because they naturally allow multilined strings. No need to insert /n between
lines.
For the author and version metadata of the add-on, the practice of assigning the
__author__ and __version__ variables is adopted:
__author__ = "John Doe"
__version__ = "1.0"
Although neither are standard and are entirely optional, they are commonly used
conventions. If youre interested, theyre both referred to in the documentation
of Epydoc [8], the usage of the __version__ convention is also documented in
PEP 396 [12].
Add-on definition
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.
def helloWorld():
from empro import gui
gui.MessageBox.information("Hello World!", "Hi There ...",
gui.Ok, gui.Ok)
def _defineAddon():
from empro.toolkit import addon
return addon.AddonDefinition(
menuItem=addon.makeAction('Hello World', helloWorld)
)
Most add-ons will probably require some sort of a dialog box for the user to
enter some parameters. This is demonstrated in Recipe 6.2.
showParameterSweepDialog starts by instantiating a new SimpleDialog with
two command buttons, of which the OK button is renamed. A label is added on
line 28 to display some help instructions for the user. Instead of repeating
yourself and retyping the add-ons documentation, you can simply reuse the
modules docstring. Thats easy enough, because the special variable name
__doc__ contains it. Just strip the whitespace and youre done. Next, a
drop-down list is added filled with the names of all the editable parameters.
Finally, three ExpressionEdit fields are added which are similar to a normal
text edit field but optimized for editing expressions.
Each time we select another parameter, youd like to update the unitClass of
the expression editors and preserve the display value while doing so. If the
current unit is mm and you enter the value 10, youll see 10 mm. Its display
value is 10, but its real value in references units (meters) is 0.01. If you simply
change the unit class to frequency with GHz as display unit, youll get
1e-11 GHz instead of the 10 GHz as you would have expected. To fix that, in
onParameterSelected you first check if the expressions formula is a literal
number by feeding it to the built-in float operator (line 71). Any more complex
formula with operators and units will raise a ValueError and is simply ignored.
If it succeeds however, you now have the value in reference units. Convert it to
display units with fromReferenceUnits. Then, convert it back to the reference
units of the new unit class with toReferenceUnits and set it as the new
To use it, you select the Parameter over which you want to sweep,
and set the Start, Stop and Step values (which may be expressions).
Finally, you click on Create & Queue Simulations.
A new simulation will be created for each of the values in the range
Start + k * Step for k = 0,1,2,... until Stop is reached.
'''
def showParameterSweepDialog():
import empro
from empro import core, gui, units
layout = dialog.layout
layout.add( gui.Label( __doc__.strip() ))
# for the parameter selector, add a label and a combobox.
parameterWidget = gui.Widget()
parameterLayout = parameterWidget.layout = gui.HBoxLayout()
parameterLayout.spacing = 0
parameterLayout.addWidget( gui.Label("Parameter"))
parameterCombo = gui.ComboBox()
parameterLayout.addWidget( parameterCombo )
layout.add(parameterWidget)
# fill the combo box with all editable parameters.
parameters = empro.activeProject.parameters()
for name in parameters.names():
if not parameters.isEditable(name):
continue
parameterCombo.addItem(name)
def _defineAddon():
from empro.toolkit import addon
return addon.AddonDefinition(
menuItem=addon.makeAction('Simple Parameter Sweep',
showParameterSweepDialog,
icon=":/application/ParameterSweep.ico")
)
Add-ons can also be used to add new menu items to the context menu of the
items in the Project Tree. To demonstrate how, the function sheetFromWireBody
from Recipe 2.3 is taken and turned it into an add-on so it can be applied to any
Wire Body from the user interface.
coverWireBody is the heart of the add-on. Its role is similar to
coverAllWireBodies, but it only replaces a single Wire Body. It searches the
projects geometry for the Assembly containing wirebody on line 34. Then, it
figures out the index within assembly so it can replace the original part by the
covered copy. If it fails to find the index, a ValueError exception is raised, and
covered is simply appended.
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
def _doCoverWireBody(selection=None):
'''
Function called when the menu item is clicked.
'''
def _defineAddon():
from empro.toolkit import addon
return addon.AddonDefinition(
menuItem=addon.makeAction("&Cover Wire Body",
_doCoverWireBody,
icon=":/geometry/CoverWirebody.ico"),
onContextMenu=_onContextMenu
)
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