.. include:: definitions.rstinc .. _agx_python_scripting: ==================== Python Scripting ==================== .. contents:: :depth: 1 :local: Python (`www.python.org `_) is a powerful and popular scripting language for scientific computing and engineering. The main reason behind this is the the vast number of libraries, toolkits and SDK’s available. The AGX Python library comes as SWIG-generated modules and extensions, mirroring the C++ native API as far as possible. Each C++ namespace that is part of the AGX C++ SDK and exposed to Python makes up a Python module. .. include:: python_readme.rstinc ----------------------------- Modules and Library structure ----------------------------- Each C++ namespace of AGX exposed to Python is exposed as its own Python module available for import. For example: agxSDK is encoded in _agxSDK.pyd. These modules are found together with their respective native extension shared library in: - In Windows, beneath the directory of your AGX installation: `/bin/x64/agxpy` - In Unix the default path should be something similar to: `/usr/local/lib/python3.5/dist-packages/site-packages/` This might be different on your specific Unix flavor. For Python to be able to locate the AGX Python modules, you might need to set the PYTHONPATH environment variable. See the chapter Environment below. You can verify the AGX Python installation by creating a Python script that contains the following: .. code-block:: py import agx agx.init() If this works, then Python can locate the required libraries for AGX Python. .. _python-agx-integration: ---------------------------------- Python/AGX Dynamics integration ---------------------------------- .. TIP:: File script `/data/python/template.agxPy` works as a starting point for a new script using the AGX Dynamics Python API. AGX Dynamics and Python can be used together in two different ways: 1. In the *native* Python scripting environment using the command: `python` 2. Embedded as a scripting format for the `agxViewer.exe` application Both ways are simple to support from the same Python application or library, as long it is initialized correctly. Aside from importing the required Python modules used by the program, the following bare-bone example will determine how the script is being executed; whether its embedded mode from agxViewer or in a native Python environment: .. code-block:: python import agx import agxSDK import agxIO # import of additional modules ... import agxPython init = agx.AutoInit(); # Important so that we initialize the AGX SDK def buildScene1(sim, app, scene_root): # build your AgX scene here by adding to the agxSDK.Simulation # object sim, aagxOSG.ExampleApplication object app and # the agxOSG.Group object scene_root. Below we have the entry point for `agxViewer`, or an application based on :code:`agxOSG::ExampleApplication`. It will look for the function :code:`buildScene()` and execute its content. .. code-block:: python def buildScene(): # Script is run from agxViewer. Everything is set up, so # all we need to do is fetching our sim, app and osg root node # and call the function which builds our scene sim = agxPython.getContext().environment.getSimulation() app = agxPython.getContext().environment.getApplication() scene_root = agxPython.getContext().environment.getSceneRoot() # That is all we need before we teleport...x buildScene1(sim, app, scene_root) Below, we have the entry point for when the script is executed with the `python` executable. It will find that the :code:`agxPython.getContext()` return :code:`None` and call the main function. .. code-block:: python def main(fileName): # Initialize the AGX ExampleApplication here (see any script in data/python for specifics) app = agxOSG.ExampleApplication() # Create a command line parser. argParser = agxIO.ArgumentParser() argParser.readArguments(arg) ## feed the parser with the arguments # Add a scene to the application. Whenever the key '1' is pressed, this file will be reloaded # and the def "buildScene" is called app.addScene(fileName, "buildScene1", ord("1")) # Call the init method of ExampleApplication # It will setup the viewer,windows etc. if app.init(argParser): initCamera(app) app.run() else: print("An error occurred while initializing ExampleApplication.") # Test to see if we are running via the native python interpretator or in embedded mode (agxViewer) if agxPython.getContext() == None: # Script is run from native Python interpreter, call main with the script file name # as argument main(sys.argv[0]) .. _python-cpp: ----------------------------------- Using Python for modelling in C++ ----------------------------------- It is also possible to use python for modelling and C++ for control. The tutorial :code:`tutorial_python_excavator_controller.cpp` that can be located among the :ref:`C++ tutorials` illustrates this. 1. Construct the model in Python, including parameterization 2. Load the python script from C++. 3. Access constraints, bodies etc. from C++. 4. Control the simulation from C++. In this way you can get the best of both worlds: - The fast iteration/flexibility of programming with Python without compilation/linking. - Low performance overhead because no code is actually running in Python during the simulation. So this is a good way of mixing the two worlds. When running the tutorial_python_excavator_controller you can reload the python script by pressing the button '1' just as with the agxViewer application. ------------------------- AGX Python coding guide ------------------------- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Sharing AGX buffers as numpy arrays ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Internally AGX entities are stored in contiguous memory buffers. Looping through these buffers using the AGX C++ API is very fast. By creating numpy arrays, that points at the same memory as the AGX buffers, it is possible to inspect and change the AGX buffers using all the usual numpy operations, with comparable performance as the C++ API. One danger, is that every time AGX is in control it can decide to reallocate the buffer. From python the user is never notified about this. Therefore, the user must recreate the numpy array between every timestep. This process is made more convenient by using the ``BufferWrapper`` python class implemented in ``agxPythonModules``. The wrapper finds the specified buffer and creates a numpy array with the correct type and size that wraps around it. Every time the numpy array is accessed from the wrapper by the user, the wrapper class recreates the array around the buffer. .. code-block:: py from agxPythonModules.utils.numpy_utils import BufferWrapper # ... # Get the position buffer for all the particles in sim position_buffer = BufferWrapper(sim, "Particle.position") position_array = position_buffer.array # Get the shape of the array shape = position_array.shape # Set the z-position of every particle position_array[:,2] = np.linspace(-10, 10, shape[0]) # step agx sim.stepForward() # re-fetch buffer in case agx has reallocated position_array = position_buffer.array # Print the z position for every particle. print(position_array[:,2]) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Proxy classes, their instances and reference counting ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ All C++ classes exposed to python will be accessible through a proxy class. Each namespace of AGX will belong to a separate Python module. For example the class ``agx::RigidBody`` will be located in the module _agxPython which is imported to Python using the *import* keyword. Below is an example of how to import the agx namespace and instantiate a class in that namespace: .. code-block:: py import agx rb = agx.RigidBody() C++ classes derived from agx::Referenced are automatically handled by Python, so that as long as there is a C++ :code:`agx::ref_ptr` *and/or* a Python object referring to it, it will not be deallocated. ^^^^^^^^^^ Attributes ^^^^^^^^^^ Attributes added to proxy class instances in Python does not exist outside the Python object to which the attributes belong, and can’t pass through the native C++ layer back to Python again. The following code demonstrates this: .. code-block:: py rb_vector = agx.RigidBodyVector() rb = agx.RigidBody() rb.my_attr = 5.0 # An attribute added to the rb object rb_vector.append(rb) rb_retrieved = rb_vector[0] print(rb.my_attr) # Will print 5.0 print(rb_retrieved.my_attr) # Will throw AttributeError exception! Because rb_retrieved is # not the same Python object as rb The reason for this is that: *Proxy class instances, or Python objects of proxy classes, are not exact representations of C++ objects and multiple Python proxy objects may reference the same C++ object*. ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Function/method overload ambiguity ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ In Python it is forbidden to declare anything using the same identifiers or names in any scope where they already exist. In C++ however, it is commonplace to overload functions and methods by their signatures while sharing the same names. In AGX Python, proxy classes only have one representation for all possible versions of static functions or methods found in C++. Ambiguity resolving is done within the Python extensions before a match is found based on which arguments the Python code passed to it during a call, where no match throws a NotImplementedError exception back to the caller. ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Inherited classes/virtual methods ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Some AGX classes allow for their virtual methods to be overridden in derived classes, where EventListener family of classes being the most relevant example of this. The table below show examples of classes which can be implemented in Python: +-----------------------------------+---------------------------------------------------+ | Python Class | Virtual Methods (in Python) | +===================================+===================================================+ | **agxSDK.EventListener** | .. code-block:: c++ | | | | | | def addNotification(self) -> "void" | | | def removeNotification(self) -> "void" | +-----------------------------------+---------------------------------------------------+ | **agxSDK.ContactEventListener** | .. code-block:: c++ | | | | | | def preCollide( | | | self, | | | t: "float" | | | ) -> "void" | | | | | | def pre( | | | self, | | | t: "float" | | | ) -> "void" | | | | | | def post( | | | self, | | | t: "float" | | | ) -> "void" | | | | | | def last( | | | self, | | | t: "float" | | | ) -> "void" | +-----------------------------------+---------------------------------------------------+ | **agxSDK.GuiEventListener** | .. code-block:: c++ | | | | | | def mouseDragged( | | | self, | | | buttonMask: "MouseButtonMask", | | | x: "float", | | | y: "float" | | | ) -> "bool" | | | | | | def mouseMoved( | | | self, | | | x: "float", | | | y: "float" | | | ) -> "bool" | | | | | | def mouse( | | | self, | | | buttonMask: "MouseButtonMask", | | | state: "MouseState", | | | x: "float", | | | y: "float" | | | ) -> "bool" | | | | | | def keyboard( | | | self, | | | key: "int", | | | modKeyMask: "int", | | | x: "float", | | | y: "float", | | | keyDown: "bool" | | | ) -> "bool" | | | | | | def update( | | | self, | | | x: "float", | | | y: "float" | | | ) -> "void" | +-----------------------------------+---------------------------------------------------+ | **agxSDK::ContactEventListener** | .. code-block:: c++ | | | | | | def impact( | | | self, | | | time: "double", | | | geometryContact: "agxCollide::GeometryContact"| | | ) -> "KeepContactPolicy" | | | def contact( | | | self, | | | time: "double", | | | geometryContact: "agxCollide::GeometryContact"| | | ) -> "KeepContactPolicy" | | | def separation( | | | self, | | | time: "double", | | | geometryPair: "agxCollide::GeometryPair" | | | ) -> "void" | +-----------------------------------+---------------------------------------------------+ | **agxSensor::JoystickListener** | .. code-block:: c++ | | | | | | def axisUpdate(self, | | | state: "agxSensor::JoystickState", | | | axis: "int", | | | ) -> "bool" | | | | | | def buttonChanged( | | | self, | | | state: "agxSensor::JoystickState", | | | button: "int", | | | down: "bool" | | | ) -> "void" | | | | | | def povMoved( | | | self, | | | state: "agxSensor::JoystickState", | | | pov: "int" | | | ) -> "void" | | | | | | def sliderMoved( | | | self, | | | state: "agxSensor::JoystickState", | | | slider: "int" | | | ) -> "void" | | | | | | def addModification( | | | self | | | ) -> "void" | | | | | | def removeModification( | | | self | | | ) -> "void" | +-----------------------------------+---------------------------------------------------+ It’s important to not forget about the ‘self’ argument when you override any of these methods, and that the right type of any value they return is used. Violations to this are considered an error and will break your script. Furthermore, it’s important to follow Python rules as well, something known as "Zen of Python", and call the base class version of the method being overridden when you wish to return some default value. In Python, you do not wish to "override" base class functionality with polymorphic inheritance as you do in C++, instead you "enhance" with new functionality by adding instead of replacing. ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Scope locality and garbage collection ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ In Python, all declarations made in some scope are also bound to that scope once evaluated. In other words, the default scope used in Python is "local". Assigning or returning a value binds them to whatever object or scope that accepts the value, and the value will persist in memory for as long it is strongly referenced by another object or scope. If no such references exist for a value it gets garbage collected. Whether this happens right away or some time later can vary, but it is safe to assume it mustn’t be touched by any code once it becomes garbage. Reference-counted C++ objects complicate things sometimes if care is not given to avoid premature decrement reference counts of references not owned by the dereferencer. Always keep a reference around for AGX objects you create by keeping a Python object/proxy class instance alive for as long you want to be on the safe side. ^^^^^^^^^^^^^^^^^^^^^^^^^ Ref smart pointer objects ^^^^^^^^^^^^^^^^^^^^^^^^^ Sometimes it’s not always a raw pointer object you deal with but their smart pointer counterparts. The smart pointer template class instances are suffixed with Ref, so for example smart pointer class for the Geometry class is called GeometryRef and for RigidBody, it’s RigidBodyRef. Normally they expose the same methods as the original class with the addition of those declared by :cpp:`agx::ref_ptr` template class, such as get(), isValid and so on. It is not, however, possible to pass them as arguments to methods expecting objects of template argument type; e.g. if a Geometry is expected, passing GeometryRef instead is not valid. Sometimes you deal with RefVectors, and any accessors will inevitably always return Ref objects - not objects. To dereference these smart pointers, use the get() method on them, but check if they are valid before you do, or else you risk the execution of dereferencing null pointers within the C++ layer which will crash your application. Here’s an example where we define a function which extracts all the geometries associated with a rigid body and returns a Python list with raw Geometry objects: .. code-block:: py def rigidBodyGeometriesToList(rb): geometries = rb.getGeometries() # geometries is of type GeometryRefVector geometries_list = list() # the list to return for geo_ref in geometries: # geometries contain items of type GeometryRef geometries_list.append(geo_ref.get()) # geo_ref.get() gives us its Geometry return geometries_list # geometries_list now contain Geometry items ------------------- C++ to Python guide ------------------- Most primitive types, classes, functions and methods of the AGX C++ API exposed to Python have wrappers which work as one would expect in Python. For example, wherever a C++ NULL or nullptr would be used, you use None in Python. There are however some things to watch out for, especially when the philosophical and conventional similarities between C++ and Python end. Below is a number of sample cases demonstrating the syntax difference between Python and C++: ^^^^^^^^^^^^ Construction ^^^^^^^^^^^^ **C++:** .. code-block:: c++ agx::RigidBodyRef body = new agx::RigidBody(); **Python:** .. code-block:: py body = agx.RigidBody() ^^^^^^^^^^^^^^^^^^^^^^^ Calling a normal method ^^^^^^^^^^^^^^^^^^^^^^^ **C++:** .. code-block:: c++ body->getMassProperties()->setMass( 10 ); **Python:** .. code-block:: py body.getMassProperties().setMass( 10 ) ^^^^^^^^^^^^^^^^^^^^^^^ Calling a static method ^^^^^^^^^^^^^^^^^^^^^^^ **C++:** .. code-block:: c++ agx::Constraint::calculateFramesFromBody(agx::Vec3(1,2,3), agx::Vec3(1,0,0), body1, frame1, body2, frame2); **Python:** .. code-block:: py agx.Constraint.calculateFramesFromBody(agx.Vec3(1,2,3), agx.Vec3(1,0,0), body1, frame1, body2, frame2); .. include:: agx_python_modules.rstinc