mirror of
https://github.com/mpusz/mp-units.git
synced 2026-07-08 09:30:52 +02:00
docs: add how-to guide for representing an axial vector as a tensor
Show how to model a quantity the ISQ defines as a vector (e.g. angular velocity) as a skew-symmetric tensor in the user's own domain, bridged to isq::angular_velocity by an explicit hat/vee dual, and explain why the tensor form is a separate quantity rather than the ISQ one relaxed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,7 @@ quantity calculations.
|
||||
- [Pure Dimensional Analysis](pure_dimensional_analysis.md) - Use dimensions without specific units for symbolic computation, compile-time validation, and custom arithmetic types
|
||||
- [Type-Safe Indices and Offsets](typed_indices.md) - Model container indices and offsets as quantities with point origins, covering 0-based vs 1-based indexing, SI vs IEC element prefixes, and stride arithmetic
|
||||
- [Decompose a Vector Quantity into Components](decompose_vector_quantity.md) - Split a vector quantity into named, strongly-typed 1D-vector component quantities, covering the quantity hierarchy, `get<Idx>`/`get<QS>`, structured bindings, and the representation requirements
|
||||
- [Represent an Axial Vector as an Antisymmetric Tensor](tensor_representation_of_axial_vectors.md) - Model a quantity the ISQ defines as a vector (e.g. angular velocity) as a skew-symmetric tensor in your own domain, bridged to `isq::angular_velocity` with an explicit `hat`/`vee` dual
|
||||
- [Ensure Ultimate Safety](ultimate_safety.md) - Combine `constrained` reps, `constraint_violation_handler`, and `check_in_range` for guaranteed bounds enforcement
|
||||
- [Working With Nondimensionalized Physics](nondimensionalization.md) - Use mp-units alongside natural units and dimensionless quantities, covering the complete scale-in/scale-out workflow and tradeoffs
|
||||
<!-- markdownlint-enable MD013 -->
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
# Represent an Axial Vector as an Antisymmetric Tensor
|
||||
|
||||
This guide shows how to work with a quantity, such as _angular velocity_, that the ISQ
|
||||
defines as a **vector** but that your domain represents as an **antisymmetric tensor** (a
|
||||
skew-symmetric matrix). It explains why the tensor form is a _separate_ quantity rather than
|
||||
the ISQ one in bigger storage, how to define it, and how to bridge to and from
|
||||
`isq::angular_velocity` with an explicit dual map.
|
||||
|
||||
For background on the concepts used here, see:
|
||||
|
||||
<!-- markdownlint-disable MD013 -->
|
||||
- [Character of a Quantity](../../users_guide/framework_basics/character_of_a_quantity.md) -
|
||||
scalar, vector, and tensor characters
|
||||
- [Why a Quantity Has a Character](../../blog/posts/why-a-quantity-has-a-character.md) -
|
||||
why character is part of a quantity's identity
|
||||
- [Representation Types](../../users_guide/framework_basics/representation_types.md) -
|
||||
what a type must provide to act as a representation
|
||||
<!-- markdownlint-enable MD013 -->
|
||||
|
||||
!!! info "The examples use Eigen"
|
||||
|
||||
The snippets below use [Eigen](https://eigen.tuxfamily.org) through
|
||||
`#include <mp-units/integrations/eigen.h>`, the common choice in robotics and rigid-body
|
||||
dynamics. The technique is backend-agnostic: it works the same with Blaze or the built-in
|
||||
`cartesian_vector` / `cartesian_tensor`.
|
||||
|
||||
|
||||
## The Problem: the Matrix Does Not Fit the Vector Slot
|
||||
|
||||
ISO 80000-3 (item 3-12) defines _angular velocity_ as a **vector**: a scalar rate of
|
||||
rotation carrying the axis direction. So in **mp-units** it has vector character, and a
|
||||
three-component representation fits:
|
||||
|
||||
```cpp
|
||||
quantity w = Eigen::Vector3d{0.0, 0.0, 2.5} * isq::angular_velocity[rad / s]; // OK
|
||||
```
|
||||
|
||||
Robotics and rigid-body dynamics, however, routinely represent the same physical
|
||||
quantity as the antisymmetric **angular velocity tensor** `Ω`, the 3x3 skew-symmetric
|
||||
matrix for which `Ω r` equals `ω x r`. That matrix is an order-2 object, and it does
|
||||
**not** fit the vector slot:
|
||||
|
||||
```cpp
|
||||
Eigen::Matrix3d Omega{{ 0.0, -2.5, 0.0},
|
||||
{ 2.5, 0.0, 0.0},
|
||||
{ 0.0, 0.0, 0.0}};
|
||||
quantity bad = Omega * isq::angular_velocity[rad / s]; // error: order 2 does not fit a vector
|
||||
```
|
||||
|
||||
The rank-ordering that lets a scalar stand in for a vector does not run the other way, so
|
||||
an order-2 representation is rejected for a vector quantity. That is deliberate, and
|
||||
forcing the matrix in is the wrong fix, for reasons worth understanding before the
|
||||
solution.
|
||||
|
||||
|
||||
## Why It Is a Separate Quantity, Not `isq::angular_velocity`
|
||||
|
||||
It is tempting to reach for either of two shortcuts. Both are wrong.
|
||||
|
||||
1. **Make the tensor form the same `isq::angular_velocity`, just stored differently.** A
|
||||
quantity's character is part of its _identity_, and the ISQ defines _angular velocity_
|
||||
only as a vector. The matrix is a different mathematical object, the dual of the vector,
|
||||
related to it by an explicit map, not the same quantity in a larger representation. Were
|
||||
it the same quantity, it would also have to flow through the vector's arithmetic (for
|
||||
example `angular_momentum = moment_of_inertia * angular_velocity` yields the right vector
|
||||
`L` only for the vector `ω`, never for the matrix `Ω`).
|
||||
2. **Make it a child of `isq::angular_velocity`.** The ISQ does model a vector as a child
|
||||
of a scalar (`displacement` under `length`, `velocity` under `speed`),
|
||||
so a higher-order child is not forbidden in itself. But there the scalar parent is the
|
||||
_magnitude_ of the vector child: `speed` is `|velocity|`, a genuine refinement where
|
||||
the child adds a direction and the parent is recovered by the norm. The tensor and the
|
||||
vector _angular velocity_ are not in that relationship. They carry the _same_
|
||||
information, related by a bijective dual (the Hodge star), and the vector is _not_ the
|
||||
magnitude of the tensor (that norm is `√2·|ω|`). Modeling a lateral dual as a vertical
|
||||
refinement is a category error, and it inverts generality: across dimensions the tensor
|
||||
is the general object and the 3D vector the special case, so making the tensor a child
|
||||
of the vector is backwards.
|
||||
|
||||
The honest model is that the tensor form is its own quantity, defined in your domain, bridged
|
||||
to the ISQ vector by an explicit dual. See
|
||||
[Why a Quantity Has a Character](../../blog/posts/why-a-quantity-has-a-character.md) for the
|
||||
full reasoning.
|
||||
|
||||
|
||||
## Define the Tensor Quantity
|
||||
|
||||
Declare a quantity with the same defining equation as _angular velocity_ (angle over
|
||||
duration, hence the same dimension) but **tensor** character. It is a sibling of
|
||||
`isq::angular_velocity` by construction, not a child of it:
|
||||
|
||||
```cpp
|
||||
inline constexpr struct angular_velocity_tensor :
|
||||
quantity_spec<isq::angular_measure / isq::duration, quantity_tensor_order::tensor> {} angular_velocity_tensor;
|
||||
```
|
||||
|
||||
An `Eigen::Matrix3d` (or a Blaze matrix, or the built-in `cartesian_tensor`) now satisfies
|
||||
it: the order-2 representation that `isq::angular_velocity` rejects finally has a home. A
|
||||
tensor quantity also accepts scalar and vector representations, since a scalar and a
|
||||
vector are tensors of order 0 and 1 and rank-ordering lets a lower order fill a higher
|
||||
slot. The restriction runs only the other way, which is exactly why the vector
|
||||
`isq::angular_velocity` turns the order-2 matrix away.
|
||||
|
||||
|
||||
## The `hat` Map: Vector to Tensor
|
||||
|
||||
`hat` (written `∧` in the literature) builds the skew-symmetric matrix from the vector's
|
||||
components:
|
||||
|
||||
```cpp
|
||||
inline constexpr auto rad_per_s = si::radian / si::second;
|
||||
|
||||
[[nodiscard]] auto hat(const quantity<isq::angular_velocity[rad_per_s], Eigen::Vector3d>& w)
|
||||
{
|
||||
const auto& c = w.numerical_value_ref_in(rad_per_s);
|
||||
Eigen::Matrix3d m{{ 0.0, -c.z(), c.y()},
|
||||
{ c.z(), 0.0, -c.x()},
|
||||
{-c.y(), c.x(), 0.0}};
|
||||
return m * angular_velocity_tensor[rad_per_s];
|
||||
}
|
||||
```
|
||||
|
||||
The result is a `quantity` of `angular_velocity_tensor`, so it can never be mistaken for the
|
||||
vector it came from.
|
||||
|
||||
|
||||
## The `vee` Map: Tensor to Vector
|
||||
|
||||
`vee` (written `∨`) is the inverse, reading the three independent components back out:
|
||||
|
||||
```cpp
|
||||
[[nodiscard]] auto vee(const quantity<angular_velocity_tensor[rad_per_s], Eigen::Matrix3d>& w)
|
||||
{
|
||||
const auto& m = w.numerical_value_ref_in(rad_per_s);
|
||||
Eigen::Vector3d v{m(2, 1), m(0, 2), m(1, 0)};
|
||||
return v * isq::angular_velocity[rad_per_s];
|
||||
}
|
||||
```
|
||||
|
||||
`vee` returns an ordinary `isq::angular_velocity`, so the value re-enters the ISQ the moment
|
||||
you cross back.
|
||||
|
||||
|
||||
## Interoperating with the ISQ
|
||||
|
||||
Convert explicitly at the boundary. Work in the tensor form where your algorithm wants a
|
||||
matrix, and call `vee` before handing the value to code (or to ISQ equations) written for
|
||||
the vector _angular velocity_:
|
||||
|
||||
```cpp
|
||||
quantity w = Eigen::Vector3d{1.0, 2.0, 3.0} * isq::angular_velocity[rad_per_s];
|
||||
|
||||
quantity Omega = hat(w); // angular_velocity_tensor, backed by a 3x3 matrix
|
||||
// ... rigid-body computations that want the matrix form ...
|
||||
quantity w_again = vee(Omega); // back to isq::angular_velocity, ready for ISQ equations
|
||||
|
||||
static_assert(Omega.quantity_spec == angular_velocity_tensor);
|
||||
static_assert(w_again.quantity_spec == isq::angular_velocity);
|
||||
```
|
||||
|
||||
The conversion is visible in the source, which is the point: the vector and the tensor are
|
||||
different objects, and the dual between them is a real operation with content (in 3D it is
|
||||
the Hodge star), not a silent coercion.
|
||||
|
||||
|
||||
## What You Keep and What You Give Up
|
||||
|
||||
- **You keep** full character safety on both forms. The matrix cannot be used where a vector
|
||||
_angular velocity_ is expected, and vice versa, until you convert.
|
||||
- **You give up** automatic participation of the tensor form in the built-in ISQ equations.
|
||||
Those are written against the vector, so you bridge with `vee` (or `hat`) at the boundary.
|
||||
This is the honest cost of the two being genuinely different objects.
|
||||
|
||||
The same pattern applies to any axial-vector quantity your domain prefers to carry as an
|
||||
antisymmetric tensor, such as _angular momentum_ or _moment of force_. Define the tensor
|
||||
sibling, then provide its `hat` / `vee` dual to `isq::angular_momentum` or
|
||||
`isq::moment_of_force`.
|
||||
|
||||
|
||||
## See Also
|
||||
|
||||
<!-- markdownlint-disable MD013 -->
|
||||
- [Why a Quantity Has a Character](../../blog/posts/why-a-quantity-has-a-character.md) -
|
||||
why the tensor form is a separate quantity, not the ISQ one relaxed
|
||||
- [Character of a Quantity](../../users_guide/framework_basics/character_of_a_quantity.md) -
|
||||
scalar, vector, and tensor characters and their operations
|
||||
- [Decompose a Vector Quantity into Components](decompose_vector_quantity.md) -
|
||||
the complementary direction, naming the axes of a vector quantity
|
||||
- [Representation Types](../../users_guide/framework_basics/representation_types.md) -
|
||||
the requirements a representation must meet
|
||||
<!-- markdownlint-enable MD013 -->
|
||||
@@ -230,6 +230,7 @@ nav:
|
||||
- Pure Dimensional Analysis: how_to_guides/advanced_usage/pure_dimensional_analysis.md
|
||||
- Type-Safe Indices and Offsets: how_to_guides/advanced_usage/typed_indices.md
|
||||
- Decompose a Vector Quantity into Components: how_to_guides/advanced_usage/decompose_vector_quantity.md
|
||||
- Represent an Axial Vector as an Antisymmetric Tensor: how_to_guides/advanced_usage/tensor_representation_of_axial_vectors.md
|
||||
- Ensure Ultimate Safety: how_to_guides/advanced_usage/ultimate_safety.md
|
||||
- Working With Nondimensionalized Physics: how_to_guides/advanced_usage/nondimensionalization.md
|
||||
- Integration:
|
||||
|
||||
Reference in New Issue
Block a user