Skip to content

Commit

Permalink
[doc] Add documentation about mc_rtc::Schema
Browse files Browse the repository at this point in the history
  • Loading branch information
gergondet committed Oct 3, 2023
1 parent 52e3b3f commit ef9103b
Show file tree
Hide file tree
Showing 6 changed files with 478 additions and 0 deletions.
2 changes: 2 additions & 0 deletions doc/_data/tutorials.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@
id: ros
-
id: global-plugins
-
id: schema
-
id: tools
tutorials:
Expand Down
2 changes: 2 additions & 0 deletions doc/_i18n/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,8 @@ tutorials:
title: ROS integration
global-plugins:
title: Using global plugins
schema:
title: Schema structures integrated to the framework
tools:
title: Framework tools
desc: These tutorials introduce useful tools developed around mc_rtc
Expand Down
242 changes: 242 additions & 0 deletions doc/_i18n/en/tutorials/usage/schema.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
## Introducing `mc_rtc::Schema`

JSON Schema is a [specification](https://json-schema.org/) that allows to document the expected data format to go from a JSON object to a given data structure.

It is used in [mc_rtc](https://jrl-umi3218.github.io/mc_rtc/json.html) to document the expected data for loading objects -- e.g. tasks or constraints -- into the framework.

As you write code within mc_rtc you will likely encounter a scenarion where you load a number of parameters from an {% include link_tutorial.html category="usage" tutorial="mc_rtc_configuration" %} object.

In addition to loading this configuration object into your data structure, you will also likely be interested in:

- Saving the current state of the object to an `mc_rtc::Configuration` file -- and then to disk;
- Allow to edit the parameters online;
- Document the configuration format;

Writing all this code by hand is possible but tedious and error-prone, especially when adding or removing fields from the structure.

This is where `mc_rtc::Schema` comes in. Its objective is to allow you to write a simple structure that behaves like a simple structure but comes packed with extra features.

## Our first schema-based structure

The following example is a simple schema structure that illustrates the basic usage of the feature:

```cpp
#include <mc_rtc/Schema.h>

struct SimpleSchema
{
MC_RTC_NEW_SCHEMA(SimpleSchema)
#define MEMBER(...) MC_RTC_SCHEMA_REQUIRED_DEFAULT_MEMBER(SimpleSchema, __VA_ARGS__)
MEMBER(bool, useFeature, "Use magic feature")
MEMBER(double, weight, "Task weight")
MEMBER(std::vector<std::string>, names, "Some names")
MEMBER(sva::ForceVecd, wrench, "Target wrench")
MEMBER(sva::PTransformd, pt, "Some transform")
#undef MEMBER
};
```
<h5 class="no_toc">Note for MSVC users</h5>
If you intend the above code to be used with the Microsoft Visual C++ Compiler (MSVC), the `MEMBER` definition should be changed to:
```cpp
#define MEMBER(...) MC_RTC_PP_ID(MC_RTC_SCHEMA_REQUIRED_DEFAULT_MEMBER(SimpleSchema, __VA_ARGS__))
```

Or avoid the `MEMBER` helper all-together.

This works around a pre-processor issue in MSVC.

### Usage

The structure that we defined this way is used a regular C++ structure:

```cpp
void do_foo(const sva::ForceVecd &);

// The structure can be default constructed (more on the defaults later)
SimpleSchema simple;
// Access a member as a simple structure
do_foo(simple.wrench);
// The members are of the type you specified so this is also possible
do_foo(simple.pt * simple.wrench);
```
However, this structure also has a number of extra functionalities built-in:
```cpp
// Load from an mc_rtc::Configuration value
simple.load(config);
// Save to an mc_rtc::Configuration object
simple.save(config);
// We can also take advantage of mc_rtc::Configuration user defined conversions
config.add("simple", simple);
SimpleSchema simple2 = config("simple");
// Print to the console
mc_rtc::log::info("simple:\n{}", simple.dump(true, true));
// Add a form to the GUI that will edit simple in place
simple.addToGUI(*controller.gui(), {"Form category"}, "Form name",
[this]() {
// The simple object has been updated with new values at this point
on_simple_update();
});
```

You can also use aggregate initialization or [designated initialization](https://en.cppreference.com/w/cpp/language/aggregate_initialization#Designated_initializers) (from C++20) to initialize the structure:

```cpp
SimpleSchema simple{true, 42.42, {"a", "b"}, wrench, pt};
```
Finally the structure we created here has the same size as the simpler structure we could have created by hand.
## The `MC_RTC_SCHEMA_MEMBER` macro
In the previous example, we have used the `MC_RTC_SCHEMA_REQUIRED_DEFAULT_MEMBER` macro. It is actually a wrapper around the more general `MC_RTC_SCHEMA_MEMBER` macro which has the following siganture:
```cpp
MC_RTC_SCHEMA_MEMBER(T, TYPE, NAME, DESCRIPTION, FLAGS, DEFAULT, ...)
```

The parameters have the following usage:

- `T` is the type of the Schema object where this member appears;
- `TYPE` is the type of the member variable;
- `NAME` is the name of the member variable;
- `DESCRIPTION` is a documentation string for the member variable, it is also used in the generated Form;
- `FLAGS` is used to add additional information about the member, most notably whether the member is required or not;
- `DEFAULT` is the default value used for this member;
- `...`/`NAMES` these extra parameters are used for two purposes:
- if `TYPE` is `std::string` this can be used to provide valid values;
- if `TYPE` is `std::variant<T...>` this is used to assign meaningful names to the variant altneratives;

### About `TYPE`

The intent is for any `TYPE` to be usable -- at least for the load/save scenario, not all types may be supported for the form-based edition.

If a given `TYPE` is not supported you will get a compilation error, please report the issue to mc_rtc developpers.

As of now, most types that can be loaded/saved through an `mc_rtc::Configuration` object are supported as well as schema-based types and `std::vector` of such types.

The following is an example using such types:

```cpp
struct ComposeSchema
{
MC_RTC_NEW_SCHEMA(ComposeSchema)
#define MEMBER(...) MC_RTC_PP_ID(MC_RTC_SCHEMA_REQUIRED_DEFAULT_MEMBER(ComposeSchema, __VA_ARGS__))
MEMBER(int, integer, "Integer value")
MEMBER(SimpleSchema, simple, "Simple schema")
MEMBER(std::vector<SimpleSchema>, many, "Multiple simple schema")
#undef MEMBER
};
```
### About `FLAGS`
There is current two flags:
- `mc_rtc::schema::Required`
- `mc_rtc::schema::Interactive`
#### `mc_rtc::schema::Required`
When a member is required:
- the member must be present in the configuration;
- the member must correctly de-serialize to its type;
Otherwise the member is only read if it is present in the configuration.
Note: members are always saved into a Configuration object, whether they are required or not
#### `mc_rtc::schema::Interactive`
The schema can treat the following types as interactive:
- `Eigen::Vector3d`
- `sva::PTransformd`
If members of these types are added to a form then interactive form elements will be used.
However, it does not always make sense. For example, a 3D gain could be represented with an `Eigen::Vector3d` and there is no point to use a 3D marker to edit this value.
You can control this behavior by setting the interactive flag accordingly. In all macros, except the basic `MC_RTC_SCHEMA_MEMBER`, the flag is set by default.
#### Example
The following example illustrates the use of these flags in the `MC_RTC_SCHEMA_MEMBER` macro call:
```cpp
struct InteractiveSchema
{
MC_RTC_NEW_SCHEMA(InteractiveSchema)
#define MEMBER(...) MC_RTC_PP_ID(MC_RTC_SCHEMA_MEMBER(InteractiveSchema, __VA_ARGS__))
MEMBER(Eigen::Vector3d,
point,
"3D point",
mc_rtc::schema::Required | mc_rtc::schema::Interactive,
Eigen::Vector3d::Zero())
MEMBER(Eigen::Vector3d, gain, "3D gain", mc_rtc::schema::Required, Eigen::Vector3d::Ones())
MEMBER(Eigen::Vector3d, gainOpt, "3D optional gain", mc_rtc::schema::None, Eigen::Vector3d::Zero())
#undef MEMBER
};
```

### About `DEFAULT`

This lets you specify the default value of the member. Anything that would be valid on the right-hand side of the assignment operator is ok here.

If you are using a `_DEFAULT_` macro, this is `mc_rtc::Default<T>` which is:

- `0` for arithmetic types (thus `false` for `bool`)
- Zero for Eigen vectors
- Identity for Eigen square matrix
- Identity for `sva::PTransformd`
- Zero for `sva::ForceVecd`, `sva::MotionVecd`, `sva::ImpedanceVecd`, `sva::AdmittanceVecd`
- Empty string for `std::string`
- `mc_rtc::Default<T>` for `std::variant<T, Others...>`
- Default values for a schema-based structure

### About `NAMES`

These extra parameter has two possible use-case:

- a list of possible values when `TYPE` is `std::string`
- a description of the types when `TYPE` is `std::variant<T...>`

#### Example

In this example we have a gain represented as variant. It can be a `double` scalar or an `Eigen::Vector3d`. The `mc_rtc::schema::Choices` is passed to the `MEMBER` macro and those names will be used when displaying the choice in the GUI.

```cpp
struct SimpleVariant
{
MC_RTC_NEW_SCHEMA(SimpleVariant)
using gain_t = std::variant<double, Eigen::Vector3d>;
#define MEMBER(...) MC_RTC_PP_ID(MC_RTC_SCHEMA_REQUIRED_DEFAULT_MEMBER(SimpleVariant, __VA_ARGS__))
MEMBER(gain_t, stiffness, "Task stiffness", mc_rtc::schema::Choices({"scalar", "dimensional"}))
#undef MEMBER
};
```
## The `MC_RTC_NEW_SCHEMA` and `MC_RTC_SCHEMA` macros
In all examples so far we have seen usage of `MC_RTC_NEW_SCHEMA`. As the name suggests, this macro is used to introduce a new schema declaration.
It takes a single argument which is the name of the structure we are creating.
However, it is sometimes useful to extend a schema with additional members. In such cases one should use the `MC_RTC_SCHEMA` macro instead:
```cpp
struct ExtendedSchema : public SimpleSchema
{
MC_RTC_SCHEMA(ExtendedSchema, SimpleSchema)
#define MEMBER(...) MC_RTC_PP_ID(MC_RTC_SCHEMA_REQUIRED_DEFAULT_MEMBER(ExtendedSchema, __VA_ARGS__))
MEMBER(double, extendedGain, "Gain for the extended algorithm")
#undef MEMBER
};
```

This macro simply takes the schema name as well as the base class the schema is using.
2 changes: 2 additions & 0 deletions doc/_i18n/jp.yml
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,8 @@ tutorials:
title: ROS統合
global-plugins:
title: グローバルプラグインの利用
schema:
title: フレームワークに統合されたスキーマ構造
tools:
title: ツール
desc: これらのチュートリアルはmc_rtc関連の便利なツールを紹介します。
Expand Down
Loading

0 comments on commit ef9103b

Please sign in to comment.