Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/examples/adc-millivolts.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Analog-to-digital converter counts to millivolts
# Analog-to-digital converter: counts to millivolts

A 12-bit analog-to-digital converter (ADC) measures voltages against a 3300 millivolt (mV)
reference, and reports them as integer counts from 0 to 4095. One count --- one least significant
Expand Down
203 changes: 203 additions & 0 deletions docs/examples/eigen-kinematics.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
# Eigen: 3D vector kinematics

A body has a position, a velocity, and an acceleration, each a 3D vector. Where is it one timestep
later, and how far from the origin is it?

<!--
AUTHORING NOTE. The two tabs below are a blink comparison: readers flip between them and compare
corresponding lines in place. Keep each banner to one short line, or the code will start at
a different height in each tab and the comparison stops working. The real explanation belongs
under "What's happening", not in the banner.
-->

=== "⚠️ Before: raw C++"

⚠️ **Before** --- every vector is a bare `Vector3d`, and the units live only in the names.
{ .ab-banner .ab-before }

??? note "Includes and usings"

```cpp
--8<-- "examples/eigen_kinematics/raw.cc:frontmatter"
```

```cpp
--8<-- "examples/eigen_kinematics/raw.cc:example"
```

=== "✅ After: with Au"

✅ **After** --- each vector is a `Quantity` whose rep is `Vector3d`, so the units are checked.
{ .ab-banner .ab-after }

??? note "Includes and usings"

```cpp
--8<-- "examples/eigen_kinematics/au.cc:frontmatter"
```

```cpp
--8<-- "examples/eigen_kinematics/au.cc:example"
```

!!! note
The two tabs are aligned for comparison: blank lines where one version needs fewer
statements, and extra spaces so that corresponding expressions sit in the same column.
Neither is a spelling we'd recommend writing --- they're here so that flipping between
the tabs shows only the real differences.

Both programs print the same two lines[^1]:

```
5 0 119.694 m
119.798 m
```

[^1]: All of our examples get compiled and run in CI, and we check that they produce the same
output.

## What's happening

The physics is the same on both sides: $x + v \, \Delta t + \frac{1}{2} a \, \Delta t^2$. What
changes is who is responsible for the units. The raw code has three different unit conversions:

- `km/h` to `m/s` for the velocity,
- `ms` to `s` for the timestep, and
- `g_0` to `m/s^2` for the acceleration.

The first two are at least visible in the source. The third one isn't, because the raw version
doesn't convert anything. Instead, it uses `-9.80665` directly as a magic number, leaving users to
guess the intent.

All of those conversions vanish from the source code in the Au version, because the library
automatically generates the correct conversion factors --- _at compile time_.

The names simplify, too: unit-suffixed names such as `x_m`, which force the human to keep track of
the units, get replaced by the simpler `x`. In fact, for velocity, we get _even more_
simplification: both `v_mps` and `v_kph` get replaced by a single `v`. Its units happen to be `km
/ h`, but we don't need to worry about that; we know the library will produce any necessary
conversions. These simpler names really pay off in the `advanced_position()` function body: when
the suffixes vanish, the underlying physics shows through more clearly.

The output lines simplify for the same reason. The raw version types the unit label by hand ---
`<< " m"`, twice, with nothing checking that it still matches what the number means. The Au version
streams the quantities themselves, and the label comes from the type: `norm(x_new)` is a length, so
it prints `m`, and `transpose(x_new)` is a whole vector, so it prints Eigen's formatting of the
elements followed by the one unit they all share.

### Unit symbols and constants

This example leans on [Unit symbols], such as `m` and `km`, and [Constants], such as
`STANDARD_GRAVITY`. They both have the same effect here: when you _multiply_ or _divide_ by them,
they _change the units_, but **not** the _underlying stored value_. If the input is already
a `Quantity`, you get another `Quantity`; and if it's not, then it _becomes_ one.

Unit symbols are a handy, _concise_ way to annotate your variables with their units. Writing `12.34
* m / s` has exactly the same effect as `(meters / second)(12.34)`; it's just a little shorter.

Again, keep in mind that unit symbols and constants do _not_ change the underlying value. So, if
you're following our Eigen [safety guide], these do _not_ count as "operations" that create risk for
dangling references. That's why the variable assignments here are perfectly safe, even without
`eval()`.

??? note "More nuance on lifetime risk"
To be clear: we mean that multiplying by symbols or constants doesn't _add_ lifetime risk. We
_don't_ mean unit symbols and constants _preclude_ lifetime risk. If there is _pre-existing_
lifetime risk, these won't magically remove it.

Consider this example. Suppose we have two utility functions that return `Eigen::Vector3d`
instances:

```cpp
Eigen::Vector3d v1();
Eigen::Vector3d v2();
```

The following example is guaranteed to dangle:

```cpp
auto q = (v1() + v2()) * m;
```

The sum holds references to its operands, which in this case are the temporary objects `v1()`
and `v2()`, neither of which survives past the semicolon at the end of the line. `m` doesn't
make this safe, but it's also not the root of the problem: the following simpler example is also
guaranteed to dangle!

```cpp
auto q = v1() + v2();
```

We hope this discussion clarifies how unit symbols and constants relate to lifetime risk. For
a fuller treatment of this topic, we recommend that all users read and understand the Eigen
[safety guide] before using Au with Eigen.

### Alias names

In this example, we went with `Position`, `Velocity`, and `Acceleration`. This is a fine approach,
but not the only possible one. If you want to use mixed units in your interfaces, you could also
define a custom "rep-named alias" for `Eigen::Vector3d`:

```cpp
template <typename U>
using QuantityV3 = Quantity<U, Eigen::Vector3d>;
```

Then you could write `QuantityV3<Meters>` for a position, `QuantityV3<KilometersPerHour>` (after
defining a suitable `KilometersPerHour` alias), and so on.

This question is mostly a matter of taste; Au is safe either way.

### Eigen safety

Eigen's famously fast performance comes in part from *lazy evaluation*. The equally famous _cost_
of this speed is an elevated risk of object lifetime bugs. We have a whole Eigen [safety guide]
devoted to this topic in general. We'll hit the highlights relevant to this example here.

First, it's important to appreciate that Au has "risk parity" with Eigen. This means that when you
add Au to Eigen, you still have all the same risks, but you _don't_ get _new_ ones. The [safety
guide] explains the details, but the upshot is that you should still be looking for the same warning
signs as raw Eigen, and you'll still use the same strategies to mitigate the issues (even if some of
the particulars might change, such as `eval()` being a free function instead of a member function).

In this example, there are three places where expression templates occur, and thus three places that
may carry object lifetime risk.

- The arguments `v` and `a` that we pass to `advanced_position()` both need unit conversions.
- These are safe because they're assigned to a _concrete type_: `Velocity` and `Acceleration`,
respectively.

- The _return value_ of `advanced_position()`, `x + v * dt + 0.5 * a * dt * dt`, is also an
expression template.
- This is safe because the return value is a concrete type: `Position`. Evaluation happens when
we convert to the concrete type.

- The `transpose(x_new)` that we stream on the last line is an expression template too: Eigen's
"view" functions are operations, just like arithmetic.
- This one is _not_ assigned to a concrete type. It's safe for the other reason: we consume it
inside the same full expression, so `x_new` cannot have died or changed in the meantime.

So, using concrete types for the input parameters and the return value automatically guarantees
lifetime safety at those boundaries --- and an expression you compute and consume on the spot is
safe, because nothing gets deferred past its inputs.

## Summary

Au's Eigen support makes it easier to get your units right _robustly_, and often makes your code
easier to read. Lifetime safety is the one thing it leaves exactly as it found it, for better and
for worse, so make sure you're familiar with the Eigen [safety guide] before you start using Au with

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just out of curiosity, I was curious what kind of documentation Eigen had for all this. Mostly because I was thinking this could be simplified to "before you start using Eigen" (because Au doesn't add anything).

Having checked it out, we should leave it as you wrote it. The Eigen stuff is a little more scary ("don't use auto unless you are 100% sure)...

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good find. In some ways, I think our docs are better, because they're more clear about the specific ingredients for a problem. Not just "avoid auto" (which was my mindset when I set out to write the article), but "here are the two ingredients for a lifetime bug, and you need both".

In some ways, I think we were more incentivized than Eigen to find and articulate this clarity in our docs, because auto is such a core part of using Au.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I don't know what the state of auto is in the broader industry at large. If I found "our" (good job!) docs on the safety pretty clearly written. It also helps understand template expressions pretty well. Almost seems like Eigen itself could stand to update at least two different parts of their docs into a similar type of doc.

Eigen.

## Related reading

- [Eigen how-to guide](../howto/interop/eigen.md), for creating and using Eigen-backed quantities.
- [Eigen safety][safety guide], on expression templates and object lifetime.
- [Eigen compatibility reference](../reference/eigen.md), for the full list of free functions.
- [Unit symbols], including the prefix-applier form used for `km`.
- [Constants], such as the `STANDARD_GRAVITY` used here.
- [Element access](../reference/quantity.md#element-access), for reading and writing one component
of a vector quantity.

[safety guide]: ../discussion/concepts/eigen_safety.md
[Unit symbols]: ../reference/unit.md#symbols
[Constants]: ../reference/constant.md
21 changes: 12 additions & 9 deletions docs/examples/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,21 @@ lines in place --- the two versions are kept line-aligned on purpose.

## The examples

- **[Analog-to-digital converter counts to millivolts](./adc-millivolts.md).** Au's secret strength
for embedded applications is not just the built-in units; it's the ability to define _custom_
units _specifically tailored to your hardware_. This gets you unit safety from the moment the
value leaves the board.
- **[Analog-to-digital converter: counts to millivolts](./adc-millivolts.md).** Au's secret
strength for embedded applications is not just the built-in units; it's the ability to define
_custom_ units _specifically tailored to your hardware_. This gets you unit safety from the
moment the value leaves the board.

- **[Angular velocity in RPM](./angular-velocity.md).** Converting a wheel's road speed into
revolutions per minute (RPM), without fussing with manual conversion factors like `2π` or `60`.

- **[Atomic units](./atomic-units.md).** Building an entire system of units on top of Au --- exact
within itself, and as accurate as physics allows at the boundary. Unlike most others, this one has
no plain-C++ counterpart: this example shows how to extend Au for a specific domain.
within itself, and as accurate as physics allows at the boundary. Unlike most others, this one
has no plain-C++ counterpart: this example shows how to extend Au for a specific domain.

- **[Linear speed to revolutions per minute](./angular-velocity.md).** Converting a wheel's road
speed into revolutions per minute (RPM), without fussing with manual conversion factors like `2π`
or `60`.
- **[Eigen: 3D vector kinematics](./eigen-kinematics.md).** Advancing a position, velocity, and
acceleration through a timestep, with `Eigen::Vector3d` as the underlying storage type of each
`Quantity`.

## How these examples are written {#front-matter}

Expand Down
16 changes: 16 additions & 0 deletions examples/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ filegroup(
":adc_millivolts_doc_sources",
":angular_velocity_doc_sources",
":atomic_units_doc_sources",
":eigen_kinematics_doc_sources",
],
# Only the docs build consumes these. Nothing here is part of the library's API.
visibility = ["//:__pkg__"],
Expand All @@ -46,6 +47,21 @@ ab_example(
expected_output = "409.256 rev / min\n",
)

ab_example(
name = "eigen_kinematics",
au_deps = [
"//au",
"//au:io",
"//au/compatibility:eigen",
"@eigen",
],
expected_output = "\n".join([
" 5 0 119.694 m",
"119.798 m",
]) + "\n",
raw_deps = ["@eigen"],
)

single_example(
name = "atomic_units",
hdrs = ["atomic_units/atomic_units.hh"],
Expand Down
4 changes: 3 additions & 1 deletion examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,9 @@ version counted raw source lines and called a visibly broken page aligned.
region by hand, in house style.

3. Register it in `BUILD.bazel` with `ab_example(...)`, giving the exact expected stdout. The Au
side needs `//au:io` in its deps if it prints (see below).
side needs `//au:io` in its deps if it prints (see below). The raw side usually has no deps at
all --- that is the point of it --- but `raw_deps` is there for a non-units library both sides
share, as `eigen_kinematics` shares Eigen.

4. Add `":<name>_doc_sources"` to the `doc_sources` filegroup at the top of `BUILD.bazel`. Both
macros define that filegroup for you; listing it there is the one explicit edit per example, and
Expand Down
9 changes: 7 additions & 2 deletions examples/defs.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -61,22 +61,27 @@ project will end up with, so it is the shape the example should build.
load("@rules_cc//cc:defs.bzl", "cc_binary", "cc_library")
load("@rules_shell//shell:sh_test.bzl", "sh_test")

def ab_example(name, expected_output, au_deps, raw_srcs = None, au_srcs = None):
def ab_example(name, expected_output, au_deps, raw_deps = None, raw_srcs = None, au_srcs = None):
"""Defines a raw-vs-Au example pair, plus the tests that keep the pair trustworthy.

Args:
name: Name of the example. Sources are read from this subdirectory.
expected_output: The exact stdout both programs must produce.
au_deps: Deps for the Au version (the raw version must have none by construction).
au_deps: Deps for the Au version.
raw_deps: Deps for the raw version. Usually empty: the point of the raw version is that it
uses no units library. A non-units dependency that both sides share is fine, though --
`eigen_kinematics` needs Eigen in both, since Eigen is the *rep*, not the units.
raw_srcs: Sources for the raw version. Defaults to `<name>/raw.cc`.
au_srcs: Sources for the Au version. Defaults to `<name>/au.cc`.
"""
raw_srcs = raw_srcs or ["{}/raw.cc".format(name)]
raw_deps = raw_deps or []
au_srcs = au_srcs or ["{}/au.cc".format(name)]

cc_binary(
name = "{}_raw".format(name),
srcs = raw_srcs,
deps = raw_deps,
)

cc_binary(
Expand Down
Loading
Loading