This article demonstrates the feasibility of integrating Lumerical tools with Python to develop complex automated workflows and perform advanced data processing/plotting. Interoperability is made possible using the Python API, a python library known as lumapi. Hereafter these terms will be used interchangeably. Users will develop methods of managing a Lumerical session, and learn techniques of initializing and editing Lumerical simulation objects from Python script.
It should be possible to store the files in any working directory; however, it is recommended to put the Lumerical and Python files in the same folder. Basic commands for appending the environment path are given in Setting the Python Before Importing, but sophisticated manipulation of the directories is beyond the scope of this article. To modify the Lumerical working directory find instructions here.
A connection to the lumapi.py file is the key to enabling the Lumerical-Python API interface. This is accomplished by importing the lumapi module, and initializing a session. When initializing a session this will consume a GUI license. Accessing simulation objects is discussed below. Using script commands, and passing data are discussed in the articles Script commands as methods - Python API, and Passing Data - Python API.
Importing Modules
Python Path is Configured
For convenience, a basic Python 3 distro is shipped with Lumerical's solvers allowing users to get started immediately; by running any of our examples from the CAD script editor where lumapi can be loaded like any module.
import lumapi
Many users would like to integrate lumapi into their own Python environment. This is as simple as importing the lumapi module; however, by default python will not know where to find it. This can be addressed by modifying the search path. Please refer to the Python foundations guide on installing modules. Essentially we allow Python to find the lumapi module, by including its parent directory in the list that import class will search. Usually, a one-time update, see explicit path import for the OS specific directories. If you are not ready or to update the Python path then either of the following methods should work
Adding the Python Before Importing
To temporarily add the lumapi directory to your path, you can use the sys.path.append() method. This is the case if you have not yet added lumapi to your search path, and it is also useful when adding directories of other helpful lsf, fsp, py files. The following code adds the lumapi folder and current file directory.
Windows:
import sys, os
#default path for current release
sys.path.append("C:\\Program Files\\Lumerical\\[[verpath]]\\api\\python\\")
sys.path.append(os.path.dirname(__file__)) #Current directory
Linux:
import sys, os
#default path for current release
sys.path.append("/opt/lumerical/[[verpath]]/api/python/")
sys.path.append(os.path.dirname(__file__)) #Current directory
Explicit Import
If you need to specify the path to your lumapi distribution then the importlib.util() is the best method for pointing to the lumapi module. This may be the case if you are working on a different branch, or comparing lumapi versions.
Windows:
import importlib.util
#default path for current release
spec_win = importlib.util.spec_from_file_location('lumapi', 'C:\\Program Files\\Lumerical\\[[verpath]]\\api\\python\\lumapi.py')
#Functions that perform the actual loading
lumapi = importlib.util.module_from_spec(spec_win) #windows
spec_win.loader.exec_module(lumapi)
Linux:
import importlib.util
#default path for current release
spec_lin = importlib.util.spec_from_file_location('lumapi', "/opt/lumerical/[[verpath]]/api/python/lumapi.py")
#Functions that perform the actual loading
lumapi = importlib.util.module_from_spec(spec_lin)
spec_lin.loader.exec_module(lumapi)
See also: How to run Lumerical API (Python) scripts in Linux – Ansys Optics
Starting a session
This is as simple as calling the relevant constructor for the Lumerical product and storing it in an object.
fdtd = lumapi.FDTD()
Since the 2023 R1.2 release, the Python API can be used remotely on a Linux machine running the interop server (see Interop Server - Remote API to configure and run the interop server). To use the remote API, an additional parameter is required when starting a session, to specify the IP address and port to use to connect to the interop server. This port has to be the starting port defined for the interop server.
This parameter is an dictionary with 2 fields, "hostname" and port.
remoteArgs = { "hostname": "192.168.215.129",
"port": 8989 }
fdtd = lumapi.FDTD(hide=True, remoteArgs=remoteArgs)
You can create multiple sessions of the same product and different products at once, as long as they all have unique names.
mode1 = lumapi.MODE()
mode2 = lumapi.MODE()
device = lumapi.DEVICE()
Each of the product's constructor supports the following named optional parameters:
- hide (default to False): Shows or hides the lumerical GUI/CAD environment on startup. If hide is set to True, all pop-ups messages that normally appear in the GUI will also be disabled. For other methods of disabling pop-up messages, see this KB article.
- filename (default empty): Launches a new application if it is empty, and will run the script if an lsf file is provided. If a project filename is provided; it will try and load the project if it can be found in the path. See the section setting the python path before importing to add folder or use the full path to load files from other directories.
# loads and runs script.lsf while hiding the application window
inc = lumapi.INTERCONNECT(filename="script.lsf", hide=True)
Import Methods
Besides defining methods/functions in Python, user can take advantage of the auto-syncing function feature in lumapi and import functions that are pre-defined in a Lumerical script file (.lsf file or format). Once the script has been excuted using the "eval" command, the methods can be called as pre-defined methods in lumapi.
Follow is an example of importing functions from a script file "testScript.lsf" and from a script format string.
fdtd = lumapi.FDTD()
# import function defined in script format string
fdtd .eval("function helloWorld() { return \"hello world\"; }\nfunction returnFloat() { return 1.; }\nfunction addTest(a, b){ return a*b; }")
print(fdtd .helloWorld())
# import function defined in the script file "testScript.lsf"
code = open('C:/XXX/testScript.lsf', 'r').read()
fdtd .eval(code)
The script can also be passed as a parameter in the constructor to define a method:
def testAddingMethodsFromConstructor(self):
app = self.appConstructor(script="any_product_script_workspace_functions_available_in_python_test.lsf")
expectedMethods = {'helloWorld'}
expectedResults = ['hello world from script file']
results = []
results.append(app.helloWorld())
self.assertEqual(results, expectedResults)
app.close()
Advanced session management
When the variables local to the function or context manager go out of scope, they will be deleted automatically, and a Lumerical session will automatically close when all variable references pointing to it are deleted.
Wrapping the session in a function
In Python you can use functions if you need to run numerous similar instances; for example, when sweeping over some optional parameters. For more information on how the important results are returned see Passing data - Python API.
def myFunction(someOptionalParameter):
fdtd = lumapi.FDTD()
...
return importantResult
Using the "with" context manager
We support Python "with" statement by giving well-defined entrance and exit behavior to Lumerical session objects in Python. If there was any error within the "with" code block, the session still closes successfully (unlike in a function). Also note that any error message you typically see in a Lumerical script environment would be displayed in the Python exception.
with lumapi.FDTD(hide=True) as fdtd:
fdtd.addfdtd()
fdtd.setnamed("bad name") ## you will see
LumApiError: "in setnamed, no items matching the name 'bad name' can be found."
...
## fdtd still successfully closes
Simulation Objects
Since the release of 2019a, Python API supports adding objects using a constructor and setting sim-objects from a constructor.
Adding simulation objects using a constructor
When adding an object, its constructor can be used to set the values of properties at creation.
fdtd.addfdtd(dimension="2D", x=0.0e-9, y=0.0e-9, x_span=3.0e-6, y_span=1.0e-6)
In Python, dict ordering is not guaranteed, so if there are properties that depend on other properties, an ordered dict is necessary. For example, in the below line of Python, 'override global monitor settings' must be true before 'frequency points' can be set.
props = OrderedDict([("name", "power"),("override global monitor settings", True),("x", 0.),("y", 0.4e-6),
("monitor type", "linear x"),("frequency points", 10.0)])
fdtd.addpower(properties=props)
If you do not have properties with dependencies you can use regular Python dictionaries.
props = {"name": "power",
"x" : "0.0",
"y" : 0.4e-6",
"monitor type" : "linear x"}
fdtd.addpower(properties=props)
Manipulating simulation-objects
When adding a new object to a Lumerical product session, a representative Python object is returned. This object can be used to make changes to the corresponding object in the Lumerical product.
rectangle = fdtd.addrect(x = 2e-6, y = 0.0, z = 0.0)
rectangle.x = -1e-6
rectangle.x_span = 10e-6
Dict-like access
The below line of python code shows an example of the dict-like access of parameters in an FDTD rectangle object.
rectangle = fdtd.addrect(x = 2e-6, y = 0.0, z = 0.0)
rectangle["x"] = -1e-6
rectangle["x span"] = 10e-6
Parent and children access
The tree of objects in a Lumerical product can be traversed using the parent or children of an object.
device.addstructuregroup(name="A")
device.addrect(name="in A")
device.addtogroup("A")
device.addstructuregroup(name="B")
device.addtogroup("A")
bRect = device.addrect(name="in B")
device.addtogroup("A::B")
# Go up two parents from the rectangle in "B" group
aGroup = bRect.getParent().getParent()
# Print the names of all members of "A" group
for child in aGroup.getChildren():
print child["name"]
Note: Python will automatically delete variables as they removed from scope, so most of the time you will not need to close a session manually. To do so explicitly, you can call the close session using. inc.close() #inc is the name of the active session |