PyPO
is a Python package that simulates the propagation of electromagnetic field distributions using the equivalent surface current method which is a method belonging to the field of physical optics (PO). This allows for vectorial propagation of beam patterns between (off-axis) quadric and planar reflector geometries. On this page, we will outline the basic design principles of PyPO
. We will discuss, from the bottom up, how the different software layers interact. The first section will describe the libraries powering PyPO
. Even though these libraries are written in C/C++/CUDA, we will try to keep the explanation accesible. However, to fully appreciate the software structure of PyPO
, basic familiarity with concepts such as C-type pointers is beneficial. The second section will focus on the bindings interface, which connects the libraries with the Python source code using the ctypes
package. The third section describes how the Python software itself is structured.
PyPO
is powered by libraries written in C/C++/CUDA. Because the physical optics calculations are quite heavy, these are written in a compiled language. The reasons for choosing C/C++ are twofold:
ctypes
,PyPO
on a system which has acces to an Nvidia card. This does necessitate an Nvidia card and GPU implementations in other frameworks (e.g. Metal for Apple) are always a welcome addition.The bottom layer takes care of the following tasks in PyPO
:
PyPO
uses the CMake build system to compile the bottom layer into so-called dynamically linked libraries (.so on Linux, .dylib on MacOS and .dll on Windows). The reason for choosing CMake is the multi-platform compilation capabilities delivered by CMake. Instead of writing Makefiles for every platform, CMake takes care of this for us by writing its own Makefiles, depending on the operating system on which PyPO
is built.
To use the compiled libraries in PyPO
we use the ctypes
package, which is part of the Python standard library. The interface between the bottom layer and top layers of PyPO
consists of data objects shared between the C/C++ layer and the Python layer. On the C/C++ side, these objects are defined as structs. On the Python side, we define classes that inherit from the ctypes.Structure
base class. The C/C++ structs and Python classes, including all members, share the same name. In this way, ctypes
exposes the data structures in both layers to one another. In general, it is preferrable to have one layer take care of all memory (de-)allocations. In PyPO
, this role is given to the Python side. This is accomplished in the following way:
ctypes.Structure
,ctypes
, where the bottom layer will do work. In essence, Python "shares" the input and output data structure with the bottom layer, while keeping ownership of the memory adresses.PyPO
data structure (see this page for more info on these PyPO
data structures).PyPO
deletes the shared input and output objects and references and frees the heap memory used by these objects.std::array
and std::vector
) in the bottom layer. This was very much doable for the C++ code. However, because of the way CUDA memory operations work, we could not help but use raw pointers every now and then in the CUDA code. So the narrative that PyPO
handles all (de-)allocation of memory is not strictly true since the CUDA code does some (de-)allocating of its own, in its own scope. It is still true though that for the mentioned input and output datatypes, PyPO
handles (de-)allocation of the resources. So, for the sake of simplicity and because the CUDA (de-)allocations happen in the local CUDA scope, we will continue as if PyPO
takes care of all (de-)allocations.To summarise, we are using ctypes
to expose the libraries so that we can call C/C++ methods from Python as if they were Python methods. For more information regarding ctypes
, we refer to the ctypes documentation.
The top layer consists of Python scripts that do a whole lot of things. Here, the high level functionalities are defined, such as calculation of performance metrics, visualisation of optical systems and results, but also setting paths to custom input beam patterns and such. For an exhaustive list, we refer to the public API reference. The top layer also keeps track of everything that is going on in a simulation. It contains a nicely formatted logger, which can log miscellaneous information to the console. Also, the top layer is where the System
is created, the interface through which PyPO
is used.
The next tutorials in the PyPO
fundamental tutorial set will dive deeper into how we can interact with the top layer.