dynibo 0.5.2

Tree-structured robot kinematics and dynamics with runtime-size workspace APIs
Documentation
# External Loads

Gravity, inverse dynamics, and forward dynamics can include resisting wrenches
at link origins. Each load pairs a model-scoped link ID with torque and force
components. The wrench is added to the generalized effort required by inverse
dynamics and subtracted from the effort available to forward dynamics; use the
opposite sign when starting from a physical force applied to the robot.

## Frame and point

A load is expressed in the selected link's local frame and applied at that link
origin. If a force is applied at an offset point, first shift it to an equivalent
wrench at the link origin. Components use torque-first order.

## Creating loads

=== "Rust"

    ```rust
    use dynibo::{IndexedLoad, Wrench};
    use nalgebra::Vector3;

    let load = IndexedLoad {
        link: tool,
        wrench: Wrench::new(
            Vector3::new(0.0, 0.0, 0.0),
            Vector3::new(0.0, 0.0, -10.0),
        ),
    };
    ```

=== "Python"

    ```python
    from dynibo import Load

    load = Load(
        link_id=tool,
        torque=(0.0, 0.0, 0.0),
        force=(0.0, 0.0, -10.0),
    )
    gravity = robot.gravity(q, [load])
    ```

=== "C++"

    ```cpp
    DyniboLoad load{
        tool,
        {0.0, 0.0, 0.0},
        {0.0, 0.0, -10.0},
    };
    const auto gravity = robot.gravity(q, {load});
    ```

=== "C"

    ```c
    const DyniboLoad load = {
        .link_id = tool,
        .torque = {0.0, 0.0, 0.0},
        .force = {0.0, 0.0, -10.0},
    };
    check(dynibo_gravity(
        robot, workspace, q, J, &load, 1, output, G));
    ```

Every link ID must come from the robot used for the calculation. Multiple loads
may target the same or different links; dynibo accumulates their contribution.

## Reusable loads

Rust and Python robots provide `load_buffer()`. It reserves capacity once for
one load per link. `set` replaces a load, `add` accumulates it, and `remove` or
`clear` retain capacity. Rejected updates leave existing loads unchanged.
Rust passes `buffer.as_slice()` to dynamics. Python accepts the buffer directly:

```python
loads = robot.load_buffer()
loads.set(tool, force=(0.0, 0.0, -10.0))
robot.gravity(q, loads=loads, out=output)
loads.clear()
```

A Python `LoadBuffer` belongs to its creating model.
It can be shared by `fork()` instances of that model.
An independently loaded robot rejects it, even when empty. The
buffer is borrowed during a native call; concurrent mutation is rejected.
Existing Python lists of `Load` remain supported and are converted per call.

## No-load calls

Rust, Python, and C++ accept an empty collection. In C, pass `NULL` only when
`load_count` is zero. The caller owns the load array, and dynibo does not retain
it after the call.