Development Guide
=================
Guide for how to build, configure, and develop SymForce.
*************************************************
Organization
*************************************************
SymForce aims to follow Python `standards `_. The core ``symforce`` package lives in the equivalently named subdirectory at the top level. Tests, documentation, etc live at the top level outside of the core package.
To import ``symforce`` add the top level to the Python path.
See the `module reference `_ for the core package structure.
*************************************************
Build
*************************************************
SymForce is primarily written in Python and C++, and is Python 3.8+ and C++14 compatible. The build
system is CMake for the C++ components, and optionally pip / setuptools on top for Python packaging.
See the Build section on the `Homepage `_ for build instructions.
*************************************************
Additional useful commands
*************************************************
SymForce also has a top level Makefile which is not used by the build, but provides some high
level commands for development:
+----------------------------------------------+--------------------------+
| Run tests which update (most) generated code | ``make test_update`` |
+----------------------------------------------+--------------------------+
| Run tests which update all generated code | ``make test_update_all`` |
+----------------------------------------------+--------------------------+
| Run tests and open coverage report | ``make coverage_open`` |
+----------------------------------------------+--------------------------+
| Build docs | ``make docs`` |
+----------------------------------------------+--------------------------+
| Build docs + open in browser | ``make docs_open`` |
+----------------------------------------------+--------------------------+
| Run the code formatter (black, clang-format) | ``make format`` |
+----------------------------------------------+--------------------------+
| Check types with mypy | ``make check_types`` |
+----------------------------------------------+--------------------------+
| Check formatting and types | ``make lint`` |
+----------------------------------------------+--------------------------+
*************************************************
Documentation
*************************************************
This documentation is built with `Sphinx `_, including automatic parsing of the code to generate a module reference using `sphinx-apidoc `_. The code uses `Google Style `_ docstrings to annotate all modules, classes, and functions. Docs pages are ``.rst`` files in ``docs``, and the Sphinx config file is ``docs/conf.py``.
There are sample `Jupyter `_ notebooks in ``notebooks``, The tutorial notebooks are built into these docs using `nbsphinx `_, for example the `SymPy Tutorial `_.
*************************************************
Logging
*************************************************
SymForce uses the `logging `_ module. You can import and use the logger like this:
>>> from symforce import logger
>>> logger.warning('houston, we have a problem')
codegen_test.test_codegen_cpp():126 WARNING -- houston, we have a problem
You can configure the log level using :func:`symforce.set_log_level()` or by setting the ``SYMFORCE_LOGLEVEL`` environment variable. The default is ``logging.INFO``.
*************************************************
Testing and Coverage
*************************************************
SymForce is heavily tested, targeting close to 100% code coverage.
Tests live in ``test`` and use `unittest `_. Additionally, `coverage.py `_ is used to run tests while measuring code coverage. The generated coverage report also provides a great view into what methods need to be tested and what code is potentially unused.
| Run a specific test: ``python test/symforce_codegen_test.py``
| Run with debug level output: ``SYMFORCE_LOGLEVEL=DEBUG python test/symforce_codegen_test.py``
| Run all Python and C++ tests after building with cmake: ``cd build; ctest``
| Run all Python tests, without a cmake build (tests cannot be run in the same interpreter): ``ls test/*_test.py | xargs -n1 -P $(nproc) python``
When debugging a specific test, the use of `ipdb `_ is highly recommended, as is reproducing the most minimal example of the issue in a notebook.
*************************************************
Formatting
*************************************************
Symforce uses the `Ruff `_ formatter for Python code.
Running ``make format`` will format the entire codebase. It's recommended to develop with `VSCode `_ and integrate black or ruff.
*************************************************
Templates
*************************************************
Much of the core functionality of SymForce is in generating code using the `Jinja `_ template language. It's relatively simple and easy to use - you pass it a template file in any language and a python dictionary of data, and it spits out the rendered code.
For example template files, see `symforce/codegen/backends/cpp/templates `_.
*************************************************
Symbolic API
*************************************************
SymForce uses the `SymPy `_ API, but supports two implementations of it. The SymPy implementation is pure Python, whereas the `SymEngine `_ implementation is wrapped C++. It can be 100-200 times faster for many operations, but is less fully featured and requires a C++ build.
To set the symbolic API, you can either use :func:`symforce.set_symbolic_api()` before any other imports, or use the ``SYMFORCE_SYMBOLIC_API`` environment variable with the options ``sympy`` or ``symengine``. By default SymEngine will be used if found, otherwise SymPy.
*************************************************
Building wheels
*************************************************
You should be able to build Python wheels of symforce the standard ways. We recommend using
``build``, i.e. running ``python3 -m build --wheel`` from the ``symforce`` directory. By default,
this will build a wheel that includes local dependencies on the ``skymarshal`` and ``symforce-sym``
packages (which are separate Python packages from ``symforce`` itself). For distribution, you'll
typically want to set the environment variable ``SYMFORCE_REWRITE_LOCAL_DEPENDENCIES=True`` when
building, and also run ``python3 -m build --wheel third_party/skymarshal`` and
``python3 -m build --wheel gen/python`` to build wheels for those packages separately.
For SymForce releases, all of this is handled by the ``build_wheels`` GitHub Actions workflow. This
workflow is currently run manually on a commit, and produces a ``symforce-wheels.zip`` artifact with
wheels (and sdists) for distribution (e.g. on PyPI). It doesn't upload them to PyPI - to do that
(after verifying that the built wheels work as expected) you should download and unzip the archive,
and upload to PyPI with ``python -m twine upload [--repository testpypi] --verbose *``.
*************************************************
Adding new types
*************************************************
To add a new geo or cam type to SymForce:
#. Add a symbolic implementation of your type, to either the :mod:`symforce.geo` or
:mod:`symforce.cam` module. Add an import of your type in the ``__init__.py`` file for the
module.
#. For geo types, you should add it to the ``notebooks/storage_D_tangent.ipynb`` and
``notebooks/tangent_D_storage.ipynb`` notebooks, and use the results there for your symbolic
implementation.
#. Create a test of your symbolic type, for example ``test/geo_rot3_test.py`` or
``test/cam_linear_test.py``.
#. For geo types, register their numerical equivalents in ``ops/__init__.py``
#. Add any custom methods you'd like on the runtime numerical classes to the corresponding file in
the ``custom_methods`` directory for each backend language
#. For geo types, add them to the ``"Test implicit construction"`` and ``"Test lie group ops"`` test
cases in ``test/symforce_values_test.cc``