-
Notifications
You must be signed in to change notification settings - Fork 33
Guide: Docstrings for Tudatpy
This guide first explains what docstrings are, followed by why, when, where, and especially how to write them for TudatPy. You will also find ad-hoc docstrings templates to get you started!
NOTE:
Before diving into this guide, you should be familiar with the page: Exposing C++ to Python.
Docstrings are special comments in your code that explain what it does and how to use it.
In C++, they are written using raw string literals (R"(...)
). In python, they are written using triple quotes ("""
).
Just as an example, here is the actual tudatpy docstring for the function body_origin_link_end_id()
of the estimation_setup.observation
module.
For context: this function enables the creation of a link identifier (e.g., receiver, transmitter, etc.) for a specified body. However, this should already be evident from the docstring. 😉
} else if(name == "body_origin_link_end_id" ) {
return R"(
Function to create a link end identifier for the origin (typically center of mass) of a body.
Function to create a link end identifier for the origin (typically center of mass) of a body.
Using this option will simulate the origin of a body transmitter, receiving, etc. the observation.
Although this is typically not physically realistic, it can be a useful approximation, in particular for simulation studies.
Parameters
----------
body_name : str
Name of the body
Returns
-------
LinkEndId
A LinkEndId object representing the center of mass of a body
Examples
--------
.. code-block:: python
# Code Snippet to showcase the use of the body_origin_link_end_id
from tudatpy.numerical_simulation.estimation_setup import observation
# Input of body_origin_link_end_id are strings (name of bodies, or satellites, or ground stations, etc...)
receiver = "Earth"
transmitter = "Delfi-C3"
# Call and print observation.body_origin_link_end_id with the proper inputs (receiver, transmitter)
# a LinkEndId object is returned for both receiver and transmitter
print(observation.body_origin_link_end_id(receiver))
print(observation.body_origin_link_end_id(transmitter))
)";
- Clarity for others (and future you!): makes your code easier to understand and maintain.
- Professionalism: well-documented code is easier to share and collaborate on.
- Integration with tools: Docstrings work with IDEs, linters, and tools like help() in Python to provide instant documentation.
- When adding a new functionality
- Whenever you make use of an undocumented functionality
More in general, it's always a good time to write docstrings! 😜
As for tudatpy, the source of docstrings is located in the tudatpy/kernel/docstrings.h file. This is a very long file that, at a first glance, might seem complex, but gets very clear as soon as you start familiarizing with it. Let's give it a shot, shall we?
NOTE:
If you are already familiar with the docstrings.h file, you can skip the following "Understanding the docstrings.h file structure" and go directly to the "How to write docstrings?" section.
The docstrings.h
file might seem complex, so let's try to break down its structure here.
At a high level, the docstrings.h
file contains a bunch of else if()
statements, each relating functions to their docstrings.
Let's take a look again at the body_origin_link_end_id()
function of the observation
observation module, whose docstring we have already encountered above.
In this case, the docstring (everything contained in the R"(...)
part) is linked to its function (whose name is specified in else if (name == "...")
part).
} else if(name == "body_origin_link_end_id" ) {
return R"(
Function to create a link end identifier for the origin (typically center of mass) of a body.
Function to create a link end identifier for the origin (typically center of mass) of a body.
Using this option will simulate the origin of a body transmitter, receiving, etc. the observation.
Although this is typically not physically realistic, it can be a useful approximation, in particular for simulation studies.
Parameters
----------
body_name : str
Name of the body
Returns
-------
LinkEndId
A LinkEndId object representing the center of mass of a body
Examples
--------
.. code-block:: python
# Code Snippet to showcase the use of the body_origin_link_end_id
from tudatpy.numerical_simulation.estimation_setup import observation
# Input of body_origin_link_end_id are strings (name of bodies, or satellites, or ground stations, etc...)
receiver = "Earth"
transmitter = "Delfi-C3"
# Call and print observation.body_origin_link_end_id with the proper inputs (receiver, transmitter)
# a LinkEndId object is returned for both receiver and transmitter
print(observation.body_origin_link_end_id(receiver))
print(observation.body_origin_link_end_id(transmitter))
)";
Each function in the docstrings.h
file belongs to a given tree of namespaces.
You can picture namespaces as containers that organize code and prevent naming conflicts by grouping related identifiers (such as functions, classes, or enumerations) under a common name.
Namespaces are defined throughout the docstrings.h
file.
The tudatpy namespace serves as the root for all other namespaces, with each additional namespace branching out from it. Below is the namespace hierarchy to which the body_origin_link_end_id()
function belongs:
tudatpy
├── numerical_simulation
│ ├── estimation_setup
│ │ ├── observation
│ │ │ ├── body_origin_link_end_id
To define a new namespace in the docstrings.h
file, you can simply type:
namespace <name_of_namespace> {
this should always be followed by the definition of a function C++ function, named get_docstring()
, that returns a specific docstring based on the input name.
static inline std::string get_docstring(std::string name) {
if (name == "test") {
return "test";
}
For instance, here's how the transfer_trajectory
module, belonging to the tudatpy - trajectory_design
tree is initialized in the docstrings.h
file:
namespace tudatpy {
namespace trajectory_design {
namespace transfer_trajectory {
static inline std::string get_docstring(std::string name) {
if (name == "test") {
return "test";
}
Adding a new function to be documented is then achieved by inserting a first else if
statement, as shown below.
namespace tudatpy {
namespace trajectory_design {
namespace transfer_trajectory {
static inline std::string get_docstring(std::string name) {
if (name == "test") {
return "test";
}
else if(name == "<new_function_to_document>") {
return R"(
<write_your_docstrings_here>.
)";
}
So we have added items (functions) to our containers (namespaces). And, just like containers that need to be closed after adding items, a namespace must also be properly closed once it is opened.
This is achieved by adding the missing curly brackets for each namespace of the tree. In our example, we then need to close three curly brackets.
namespace tudatpy {
namespace trajectory_design {
namespace transfer_trajectory {
static inline std::string get_docstring(std::string name) {
if (name == "test") {
return "test";
}
else if(name == "<new_function_to_document>") {
return R"(
<write_your_docstrings_here>.
)";
}
}
}
}
Adding docstrings to a function, class or enumeration within a given namespace becomes straightforward once you establish clear rules for writing them. To ensure consistency, we follow the numpydoc documentation style. As a result, in our API reference each function or class can accept all the fields specified by numpydoc (see here for an extensive list of available fields).
While the numpydoc list of available fields is extensive, for functions we focus on four main fields:
- Description - a few sentences giving a short and an extended description. Typically, the short description is just the first sentence of the extended one (see example below).
- Parameters - description of the function arguments, keywords and their respective types.
- Returns - description of the returned values and their respective types.
- Examples - this field is meant to illustrate usage through a small, working and commented code snippet.
NOTE:
Although optional, including the Examples section is highly encouraged. Developers should take responsibility for omitting it! 😜
One example is often more valuable than words, so let's take a look (again!) at the docstrings for the body_origin_link_end_id()
function of the observation
module.
} else if(name == "body_origin_link_end_id" ) {
return R"(
Function to create a link end identifier for the origin (typically center of mass) of a body.
Function to create a link end identifier for the origin (typically center of mass) of a body.
Using this option will simulate the origin of a body transmitter, receiving, etc. the observation.
Although this is typically not physically realistic, it can be a useful approximation, in particular for simulation studies.
Parameters
----------
body_name : str
Name of the body
Returns
-------
LinkEndId
A LinkEndId object representing the center of mass of a body
Examples
--------
.. code-block:: python
# Code Snippet to showcase the use of the body_origin_link_end_id
from tudatpy.numerical_simulation.estimation_setup import observation
# Input of body_origin_link_end_id are strings (name of bodies, or satellites, or ground stations, etc...)
receiver = "Earth"
transmitter = "Delfi-C3"
# Call and print observation.body_origin_link_end_id with the proper inputs (receiver, transmitter)
# a LinkEndId object is returned for both receiver and transmitter
print(observation.body_origin_link_end_id(receiver))
print(observation.body_origin_link_end_id(transmitter))
)";
Notice that among the four fields, the Summary is the only one that doesn’t require a keyword to initialize. The other three fields are initialized using the following format:
<field_name>
------------
Moreover, the Examples
block should always specify the directive .. code-block:: python
, telling the documentation tool to treat the next block of text as code written in Python.
Examples
--------
.. code-block:: python
Classes are documented in a more minimalistic manner, focused more on code design and hierarchy and less on the functional aspects. We focus on just two fields:
- Summary - a few sentences giving an extended description
- Examples - this field is meant to illustrate usage
NOTE:
Although optional, including the Examples section is highly encouraged. Developers should take responsibility for omitting it! 😜
We distinguish between base classes and derived classes.
- Base Class: it is a class from which other (derived) classes can "inherit" properties and methods, and then add their own.
- Derived Class: inherits attributes and methods from the base class. A derived class has all members of its base class, along with its own additional properties.
Base classes have to be identified as such in the description. For instance, let's show the docstrings for the LightTimeConvergenceCriteria base class.
Notice how the first line (short description) matches the first sentence of the extended description.
Also notice how the Examples field specifies the .. code-block:: python
directive.
} else if(name == "LightTimeConvergenceCriteria") {
return R"(
Base class for criteria of light time convergence.
Base class for criteria of light time convergence.
This class is not used for calculations of corrections, but is used for the purpose of defining the light time convergence criteria.
Specific light time convergence criteria must be defined using an object derived from this class.
Instances of this class are typically created via the :func:`~tudatpy.numerical_simulation.estimation_setup.observation.light_time_convergence_settings` function.
Examples
--------
.. code-block:: python
# Code snippet to show the creation of a LightTimeConvergenceCriteria object
from tudatpy.numerical_simulation.estimation_setup import observation
# Create Default Light Time Convergence Settings (no args specified = setting default arguments)
light_time_convergence_settings = observation.light_time_convergence_settings()
# Show that it is an LightTimeConvergenceCriteria object.
print(light_time_convergence_settings)
)";
Derived classes follow the same rules as base classes and should be clearly identified as such in the description. The corresponding base class should also be mentioned.
Enumerations follow the same rules as base and derived classes and should be clearly identified as such in the description.
Unlike the previously mentioned documentations, constants are not documented in the source code directly but rather in the .rst
files. See add-docstrings-for-a-constant-with-c-backend or https://github.com/tudat-team/tudatpy/pull/240 for details.
As an additional resource, we have assembled some templates to kickstart the writing process of docstrings.
NOTE:
All templates assume you are already in the namespace and you have already defined theget_docstring()
function defined in the namespaces section.
} else if(name == "<function_name>") {
return R"(
Function for ... (short description - first line of extended description)
Function for ... (extended description)
Associated base class: :class:'<base_class>'
Parameters
----------
<arg_name> : <arg_type>
<add_description>
Returns
-------
<return_type>
<add_description>
Examples
--------
.. code-block:: python
# Code Snippet to showcase the use of the <function_name> function
<code_snippet>
)";
} else if(name == "<base_class_name>") {
return R"(
Base class for defining settings for ...
Base class for defining settings for ...
This class ...
Instances of this class are typically created via the :func:`<function_name(s)>` function(s).
Examples
--------
.. code-block:: python
# Code snippet to show the creation of a <base_class_name> object
<code_snippet>
)";
} else if(name == "<derived_class_name>") {
return R"(
Derived class for defining settings for ...
Derived class for defining settings for ...
This class, derived from <base_class_name> ...
Instances of this class are typically created via the :func:`<function_name(s)>` function(s).
Examples
--------
.. code-block:: python
# Code snippet to show the creation of a <derived_class_name> object
<code_snippet>
)";
} else if(name == "<enumeration_name>") {
return R"(
Enumeration of available link end types.
Examples
--------
.. code-block:: python
<code_snippet>
)";