COM Integration/Calling Python code

Requires Analytica Enterprise or higher

You can use the COM Integration functions in Analytica to call code written in the Python programming language. This page illustrates how.

Calling Python code

In this example, we implement an object in Python that can be instantiated and used from your Analytica model. The object instantiates as a COM object (Component Object Model), and is available using the standard COM Integration features of Analytica Enterprise.

The example used here calls a Python function found in the Python scipy.spatial library to compute a Delaunay tessellation, also know as a Delaunay triangulation, of a set of points in 2-D.

After you implement your Python object, but before you instantiate if from Analytica, you'll need to register the object with Windows, which enables COMCreateObject to find it.

Prerequisites

You'll need to install Python for Windows (this example was developed using Python 3.6), and ensure you have the following Python modules installed in your Python environment:

  • win32com
  • pythoncom
  • comtypes
  • winreg
  • win32api
  • numpy
  • scipy
  • os
  • sys
  • time
  • Analytica_Python (provided here)

How to install these is beyond the scope of this article (and beyond the scope of Lumina tech support), but is something the Python user community should be able to help you with.

The Python class

The Python code that implements the COM object is as follows:

import numpy as np
from scipy.spatial import Delaunay
import Analytica_Python

 class DelaunayCOM:
    _reg_clsid_ = "{B524651C-71B2-4521-9E9D-8CC470E51B24}"  # Do not use this CSLID! Generate your own!
    _reg_desc_ = "COM component that computes a Delaunay tesselation"   
    _reg_progid_ = "Lumina.DelaunayCOM"    
    _reg_class_spec_ = "DelaunayCOM.DelaunayCOM"
    _public_methods_ = ['Tessellation','Pause']
    _public_attrs_ = ['softspace', 'noCalls']
    _readonly_attrs_ = ['noCalls']
     
    def __init__(self):
        self.softspace = 1
        self.noCalls = 0
     
    def Pause(self):
        Analytica_Python.gBreakPump = True
            
    def Tessellation(self,pts):
        tri = Delaunay(np.array(pts))
        return tri.simplices.tolist()
     
 Analytica_Python.AddCOMClass(DelaunayCOM)
     
 if __name__ == "__main__":
    Analytica_Python.TopLevelServer()

The Tessellation method is the method that is actually called. The Pause method is optional, but is useful when debugging. The Analytica_Python module contains generic functions that assist with registering the class (or classes if you have more than one) and serving it at runtime. That module shouldn't require customization when you create your own classes.

Special COM members

Your Python class contains several members with information about the COM object.

  • _reg_clsid_ is a unique class ID, and is required for any object that will be instantiated by Analytica. (It is not required for objects that are returned from methods that are called). When you create your own class, you need to generate your own unique CLSLID -- do not reuse the one shown above, which should only ever be used with the Lumina.DelaunayCOM object. You can do this from the Online GUID Generator.
  • _reg_progid_ is the name that will be used from COMCreateObject.
  • _reg_class_spec_ is the name of the Python module that contains this class, plus a dot, plus the name of the class itself. Since this code is saved in a file named "DelaunayCOM.py, the part before the dot is DelaunayCOM.
  • _public_methods_ lists the method that are public -- that can be called from Analytica.

Registration

Before the Python object can be used externally, it must be registered. You only need to do this once. To do this, open a CMD window as an Administrator (or preferably Anaconda3 CMD window), and CD to your code directory. Make sure that when you type python --version, that the correct python installation is used. If using Anaconda, make sure you have the environment containing all the prerequisites, etc. Then type:

Python DelaunayCOM.py /regserver

where DelaunayCOM.py is the name of your code file. This sets the registry settings so that Analytica will be able to find your object.

If you ever want to uninstall/unregister your object, follow the same steps but use

Python DelaunayCOM.py /unregserver

Analytica side

To instantiate the Python object from your model, call COMCreateObject("Lumina.DelaunayCOM"). The name of your own custom class would be something different, of course (use the same that you used in _reg_progid_). This call returns a COM object, which appears in a result window as «COM Object».

When you evaluate this call to COMCreateObject and you don't have a Python process already running and listening for DelaunayCOM objects, a new Python process will be launched. This new process will live until you release the object (or if you instantiate several objects, it will live until they have all been released). Running your object from a Python interpreter interface is discussed below.

You'll probably want to use a variable to hold your object, e.g.,

Variable py := COMCreateObject("Lumina.DelaunayCOM")

Given an array of 2-D points named Pts indexed by

Index pt := 1..10
Index Dim := [1, 2]

which are show here as a graph

Points for Delaunay.png

the tessellation (triangulation) is obtained by calling the COM method using the following Analytica expression

py->Tessellation(COMArray(pts,Pt,Dim) )+1

The result is 2-D. The first index, named .dim1 indexes the resulting triangles, and the second index, named .dim2, has length 3 and indexes the 3 points defining the vertices of each triangle. Because the Python function refers to the first point as point 0, we add 1. Notice that we pass an array to the method's parameter, so COMArray is used to specify the Analytica indexes and the index order to be used by Python (and NumPy).

It is more convenient to use global indexes in Analytica for the two indexes of the result, so we drag indexes to the diagram as follows

Index Vertex_pt := 1..3
Index Triangle := ComputedBy(tessellationVertices)

and embellish our definition of tessellationVertices to reindex the result as follows:

Local tri := py->Tessellation(COMArray(pts,Pt,Dim) )[@.dim2=@Vertex_pt] + 1;
Triangle := 1..IndexLength(tri.dim1);
tri[@.dim1=@Triangle]

The result is show here

Delaunay result.png

The numbers in the cells are the point numbers. For example, the first triangle in the tessellation had the 1st, 8th and 4th points as its vertices. To graph the tessellation, we need to transform those to the coordinates of each point, done here in a new variable named Tessellation_points:

pts[Pt=tessellationVertices]

After some pivoting and setting poly-area-fill in graph setup, we see the computed triangulation (a set of non-overlapping triangles).

Delaunay tessellation.png

Running in a Python interpreter

When you evaluate COMCreateObject("Lumina.DelaunayCOM") when no Python process is listening, a new Python process is launched, the code loaded, and the object instantiated in that process. Although you'll see a window for that process, which will also show the output of any print( ) calls in your Python code, the window itself is non-interactive. The process will stick around until the last object is released. (Note: You can invalidate your py variable using InvalidateResult from a button, or by adding a space to its definition, to release the object).

When developing your Python code, it is helpful to be able to interact in a Python interpreter. To run it from an interpreter, import your code, then you can tell it to start listening for COM connections by running:

Analytica_Python.Start()
Analytica_Python.Listen()

Python starts listening for connections. At this point, you cannot interact with it because it is busy.

In your Analytica model, add a button with the following OnClick expression:

py->Pause()

When you press this button, the Python interpreter returns to the prompt. While it is at the prompt, external programs including Analytica cannot make calls because it is not listening. But you can execute Python commands as part of your debugging. When you are ready to continue, retype

Analytica_Python.Listen()

so that it starts listening again.

When you are really done with the listening and ready to exit, you should type

Analytica_Python.StopServe()

to clean up and let Windows know that it is no longer serving requests for your objects.

The single function call

Analytica_Python.serveIt()

combines Start, Listen and StopServe. If you Ctrl+C it to get the the interpreter, you can resume with Listen, but you'll also need to call StopServe at the end.

Data types

Basic scalar values like a float, string, null, int, etc., are passed automatically, with the conversion between Python and Analytica data types handles transparently for you by the COM Integration functions.

When you pass an Analytica array to a parameter of a Python method, you'll need to wrap it in a call to COMArray and specify the indexes of the array. Python receives the array as a list, or when there are two or more dimensions, as a list-of-list. The nesting order is determined by the order that you specify the indexes to COMArray, with the first index specified becoming the outer index in Python.

On the Python side, this can be immediately converted to a NumPy array using numpy.array(x), or to a Tensor using Tensor.Tensor(x). In the example Python class, we see this in the first line of the Tessellation function

tri = Delaunay(np.array(pts))

where np.array(pts) makes it a NumPy array.

When a Python method returns a list, or standard Python array (i.e., a list-of-lists), the conversion into an Analytica array happens automatically. If you don't specify the result indexes, then local indexes named .dim1, .dim2, etc., are created for you in the result. If you already have indexes for the result, you can specify them by using COMCallMethod's «resultIndex» parameter.

When returning a NumPy array or Tensor array from a Python method, your Python code needs to call its toList() method, as seen in the final line of the example

return tri.simplices.tolist()

Your methods can also return a Python object from a method call, so that the methods of that object can then be called from the Analytica model. If you write the class of this object, you'll need to ensure that it has the _public_methods_ member with a listing of the methods that can be called. Of the special COM members, that one is the only one needed. In Python you can dynamically add this method to an instantiated object even if the original class definition doesn't have it. Then, you'll need to return it as:

thePolicy = win32com.server.policy.DefaultPolicy
return pythoncom.WrapObject( thePolicy(obj) )

When Analytica receives it, it will display as «COM Object». You'll need to keep track of which object is which, so you know which methods are available on each object, since they all display as «COM Object».

Because there are many other data types that appear in Python and in various Python libraries, you may encounter some that can't be marshalled by COM. When this happens, wrap them in a Python class and return a Python object as just described. Add methods to access the internals using standard data types.

Serving multiple Python classes

The example above exposes a single COM class that can be instantiated from Analytica. If you want to expose multiple different top-level classes, you should structure your Python modules and files so that Analytica_Python.AddCOMClass( theClass ) is called for each one before the lines

if __name__ == "__main__":
Analytica_Python.TopLevelServer()

are reached, or when running in an interpreter, before the call to Analytica_Python.Start() is executed.

See Also

Comments


You are not allowed to post comments.