ArmarX and Python

Most code in ArmarX is written in C++. However, the middleware of ArmarX (Ice) enables the communication between multiple programs written in different programming languages.

To develop programs for ArmarX in Python, ArmarX provides:

  • The ArmarX Python Bindings
  • Conventions and tooling for Python projects in ArmarX packages

ArmarX Python Bindings

ArmarX provides python bindings to conveniently access Ice interfaces via Python.

See also

Python Projects in ArmarX Packages

When you write and run Python code in ArmarX packages, follow the following setup.

Directory Structure

Imagine the following situation:

  • You have a standard ArmarX package my_armarx_package.
  • Inside, you want to have a Python package my_python_package.
  • Inside the Python package, you have multiple sub-packages and modules as well as scripts you would like to run, e.g. a my_python_script.py.

For such a scenario, follow the following standard directory structure. It can be automatically generated using the command

armarx-package add python my_python_package

This will generate a Python project in the my_armarx_package/python/ directory as follows:

my_armarx_package/
- build/
- data/
- etc/
- python/
- my_python_package/ # The root directory of the python project.
- README.md
- requirements.txt # pip-style requirements file (outdated, see note below)
- setup.py
- test_requirements.txt
- my_python_package/ # The actual root package.
- __init__.py
- my_subpackage/
- __init__.py
- ...
- ...
- my_python_script.py
- my_other_python_package/
- ...
- scenarios/
- source/
  • Note the top-level directory python; it is like the source directory but for Python code.
  • The python directory contains python projects, i.e. directories which have the same name as the python package. They contain and have several meta files for documentation and setup.
  • Inside the python project directory, there is the actual python root package, i.e. a directory with a __init__.py file which can be imported. When you write import my_python_project, this is the directory being imported.
  • The internal structure of your python package is pretty much up to you.
    • One convention is to have an app python subpackage (directory with an __init__.py) that contains all scripts that are meant to be directly runnable.
  • Instead of the requirements.txt, a pyproject.toml file is now used. It is automatically generated by the command armarx-package add python my_python_package.

Setup your Virtual Environment (venv)

A virtual environment ("venv") allows you to manage a separated python environment with some installed packages that is isolated from the system environment as well as other venvs. ArmarX supports (at the moment) three kinds of environments:

  • A dedicated venv is located inside your python package (default): .../python/my_python_package/.venv/
  • A shared venv is located in a special directory next to your python packages: .../python/shared_venvs/my_shared_venv/
  • Poetry environments (deprecated)

Using Axii

Add an entry to the prepare/python section of your project's Axii module:

"prepare": {
"python": {
"packages": {
"python/my_python_package": {}
}
}
}

where python/my_python_package is the relative path to your python project.

If you need a specific Python version (other than the system default),

  1. Add the corresponding tools/python/* module to the required_modules, (e.g. tools/python/3.10). These modules export environment variables such as PYTHON_3_10 specifying the path of the corresponding Python interpreter.
  2. In the prepare/python/packages entry of your Python package, specify the python entry with the Python interpreter to use for creating the virtual environment. Here, you can use the environment variables exported by the Python module.

For example:

"prepare": {
"python": {
"packages": {
"python/my_python_package": {
"python": "$PYTHON_3_10"
}
}
}
}
"required_modules": {
"tools/python/3.10": {}
}

If you need the ArmarX Python bindings:

  1. Add armarx/python3-armarx to your module's required_modules.
  2. Change the prepare/python/packages entry to:
    "prepare": {
    "python": {
    "packages": {
    "python/my_python_package": {
    "install_editable": [
    "$armarx__python3_armarx__PATH"
    ]
    }
    }
    }
    }
    "required_modules": {
    "armarx/python3-armarx": {}
    }

An example for this can be found in the module definition if armarx/SpeechX.

Manually

You can also perform a manual installation in the way that Axii does.

A working Zeroc Ice version for Python 3.10 (and also Python 3.6) is 3.7.0. Currently, 3.7.8 is not compatible with ArmarX.

Note
Python 3.10 itself needs to be installed separately on your system beforehand. With Axii, you can install it via the module tools/python/3.10.

However, you need to comment out armarx-dev beforehand in the project's pyproject.toml.

cd your_armarx_package_name/python/your_python_project_name
# Option 1:
virtualenv --python=3.10 .venv
# Option 2 (where $PYTHON_3_10 is the path of the base Python interpreter to use):
$PYTHON_3_10 -m venv .venv
source .venv/bin/activate

You might want to check the installed versions:

python --version # Should be the selected version, in this case python 3.10.x
which pip # Should be inside the venv.

You can then continue to install your project (.) into the venv in editable mode (-e) so your root Python package can be imported. This will also install dependencies listed in the pyproject.toml.

pip install --upgrade pip
pip install -e .

To install additional packages from source in editable mode, you can run:

pip install -e $armarx__python3_armarx__PATH

Running Python Scripts from ArmarX Scenarios

The systematic setup above allows simple execution of python scripts using the armarx::PythonApplicationManager.

Let's say you want to run the my_python_script.py from the example above which runs an ArmarX
component MyPythonComponent in python.

  1. In the Scenario Manager, add a PythonApplicationManager to your scenario.
  2. Change the instance name (ÀpplicationInstance/Instance Name at the top) and Ice object name (ArmarX.PythonApplicationManager.ObjectName) to MyPythonComponent.
  3. Fill in the properties under ".py..." to point to your script.
    1. 10_ArmarXPackageName: The name of your ArmarX package, here my_armarx_package
    2. 20_PythonPackageName: The name of your python package, here my_python_package
    3. 21_PythonScriptPath: The relative path of the executed python script, inside the root package, here my_python_script.py
    4. Optional: 22_PythonScriptArgs: Arguments as you would specify on the command line
    5. Optional: 23_PythonPathEntries: Additional paths to be added to the python path (relative to the root package)
  4. For the rest, the defaults should work for most cases.
  5. Run the PythonApplicationManager.

Offering Python Services to other Components through an Ice Interface

  1. Add a library, e.g. core, to your Armarx package by running
    armarx-package add library core
  2. Create a new Ice interface file, e.g. with name MyPythonServiceInterface.ice, in the library's directory (e.g., source/my_armarx_package/core)
  3. Fill the Ice interface file with the following:

    #pragma once
    module my_armarx_package
    {
    // Optional input arguments for your python code.
    struct RunMyPythonServiceInput
    {
    bool _empty_;
    };
    // Optional output/s of your python code.
    struct RunMyPythonServiceOutput
    {
    bool success;
    string errorMessage;
    };
    interface MyPythonServiceInterface
    {
    RunMyPythonServiceOutput runMyPythonService(RunMyPythonServiceInput input);
    };
    };

    By this, you'll have an Ice interface which you can offer as a service (server) for other Python or C++ components. But as we learned before in the tutorials, we need to write the code of this function runMyPythonService in our Python/C++ code. But before doing this let us update the CMakeLists.txt file of the core library.

  4. Edit the CMakeLists.txt file of the core library at this location source/my_armarx_package/core by adding at the top of the file the following:

    armarx_add_ice_library(core_ice
    SLICE_FILES
    MyPythonServiceInterface.ice
    DEPENDENCIES
    # other_package::interfaces
    # or any other dependency
    )

    If you get linker errors involving the pthread library, add this line after the code above:

    target_link_libraries(core_ice PUBLIC pthread)

    Under the DEPENDENCIES_PUBLIC of armarx_add_library, add my_armarx_package::core_ice. So, the file will look like this at the end:

    # Comment in to add an ARON library.
    # armarx_add_aron_library(core_aron
    # ARON_FILES
    # aron/MyAronType.xml
    # )
    armarx_add_ice_library(core_ice
    SLICE_FILES
    MyPythonServiceInterface.ice
    DEPENDENCIES
    # other_package::interfaces
    # or any other dependency
    )
    # (If you get linker errors without this line:)
    target_link_libraries(core_ice PUBLIC pthread)
    armarx_add_library(core
    SOURCES
    core.cpp
    HEADERS
    core.h
    DEPENDENCIES_PUBLIC
    # ArmarXCore
    ArmarXCoreInterfaces
    ArmarXCore
    # RobotAPICore
    # my_armarx_package
    # my_armarx_package::core_aron
    my_armarx_package::core_ice
    # DEPENDENCIES_PRIVATE
    # ...
    # DEPENDENCIES_INTERFACE
    # ...
    # DEPENDENCIES_LEGACY_PUBLIC
    # ...
    # DEPENDENCIES_LEGACY_PRIVATE
    # ...
    # DEPENDENCIES_LEGACY_INTERFACE
    # ...
    )
    # Comment in to enable tests.
    # armarx_add_test(core_test
    # TEST_FILES
    # test/core_test.cpp
    # DEPENDENCIES
    # my_armarx_package::core
    # )
  5. Now, it is the time to write the code of the runMyPythonService() inside your my_python_script.py and it should look like the following code snippet:
    #!/usr/bin/env python3
    import logging
    from armarx_core import ice_manager
    from armarx_core import slice_loader
    slice_loader.load_armarx_slice(
    "my_armarx_package", "../core/MyPythonServiceInterface.ice"
    )
    # DTI = Data Transfer Interface
    import my_armarx_package as dti
    log = logging.getLogger(__name__)
    class MyPythonServiceComponent(dti.MyPythonServiceInterface):
    def __init__(self, name: str):
    super().__init__()
    self.name = name
    # Initialization ...
    def runMyPythonService(
    self,
    input: dti.RunMyPythonServiceInput,
    current=None,
    ) -> dti.RunMyPythonServiceOutput:
    log.info(f"{self.name}: {input}")
    # Implement the service function ...
    output = dti.RunMyPythonServiceOutput()
    return output
    def main():
    # This is the name under which other Ice objects (components)
    # will find this Python component:
    name: str = "my_python_service"
    from armarx_core.parser import ArmarXArgumentParser
    parser = ArmarXArgumentParser(description="MyPythonService")
    # Add arguments ...
    parser.parse_args()
    component = MyPythonServiceComponent(name=name)
    log.info(f"Created component '{name}'.")
    ice_manager.register_object(component, ice_object_name=name)
    log.info(f"Registered component '{name}'.")
    ice_manager.wait_for_shutdown()
    if __name__ == "__main__":
    main()
armarx::aron::input
ReaderT::InputType & input
Definition: rw.h:19
armarx::status::success
@ success