Files
mp-units/docs/tutorials/tutorial_12.md
T
2026-01-12 20:16:43 +01:00

369 lines
16 KiB
Markdown

# Tutorial 12: Preventing Confusion with Distinct Kinds
Many engineering domains have quantities that share the same physical dimension but represent
fundamentally different concepts. Hydraulic engineering uses "_head_"—a measure of
**_potential energy per unit weight_** expressed as an equivalent _height_—in two
incompatible ways:
- **_Fluid head_**: _Potential energy_ normalized to the actual fluid's _density_
(e.g., 2 m of mercury)
- **_Water head_**: _Potential energy_ normalized to water's _density_ (e.g., 27.2 m water
equivalent)
Both express **_energy_** using _length_ dimensions, but mixing them produces physically
meaningless results—like mixing _gauge_ and _absolute pressure_ without conversion.
Traditional code using raw `double` values allows such mistakes to silently compile.
Remarkably, even units libraries from C++ and also other programming languages cannot
prevent this error—they only check dimensional compatibility, not physical meaning.
This tutorial demonstrates how **mp-units** uses `is_kind` to create distinct quantity
**subkinds within an existing hierarchy**—a capability unique among units libraries worldwide.
The key insight: `is_kind` lets quantities **inherit** properties (dimension, unit) from a
parent while **isolating** them from each other. Just as the library prevents mixing
_plane angles_ and _solid angles_ (both subkinds of _dimensionless_), you can create
custom subkinds like _fluid head_ and _water head_ (both subkinds of _height_) that
cannot be accidentally mixed.
## Problem Statement
Consider a pump system design for a chemical processing plant. Engineers must verify that
the _pump capacity_ is adequate for the fluid being handled. This requires comparing:
- **System requirement**: The fluid column that must be lifted, expressed as _fluid head_
(_energy_ normalized to the actual fluid's _density_)
- **Pump specifications**: Rated in _water head_ (_energy_ normalized to _water density_)
The relationship between _fluid head_ and _water head_ reflects _energy conservation_
with different _density_ normalizations:
$$H_{water} = H_{fluid} \cdot SG$$
$$H_{fluid} = \frac{H_{water}}{SG}$$
Where _specific gravity_ is the dimensionless ratio of _fluid density_ to _water density_.
The same _potential energy_ is expressed as a larger _height_ for lighter fluids (water)
and a smaller _height_ for denser fluids (mercury).
Here's how different approaches handle (or fail to handle) this scenario:
=== "Raw doubles"
```cpp
// Traditional approach - all heights are just doubles
double h_mercury_m = 2.0; // Height of mercury column
double h_pump_rating_m = 10.0; // Pump rated for water
double sg_mercury = 13.6; // Specific gravity
// Direct addition - compiles but physically wrong!
double total_head = h_mercury_m + h_pump_rating_m; // 12 m - WRONG!
// This treats 2 m of mercury as if it were 2 m of water
// Correct calculation requires manual tracking:
double h_mercury_as_water = h_mercury_m * sg_mercury; // 27.2 m
// Compare system requirement vs pump capacity
if (h_mercury_as_water > h_pump_rating_m) {
std::cout << "Pump is undersized!\n"; // This will trigger!
}
```
=== "Boost.Units (C++)"
```cpp
#include <boost/units/systems/si.hpp>
using namespace boost::units;
using namespace boost::units::si;
quantity<length> h_mercury = 2.0 * meters;
quantity<length> h_pump_rating = 10.0 * meters;
double sg_mercury = 13.6;
// Direct addition - compiles but physically wrong!
quantity<length> total_head = h_mercury + h_pump_rating; // WRONG!
// Both are lengths, so Boost.Units allows this
// Correct calculation still requires manual tracking:
quantity<length> h_mercury_as_water = h_mercury * sg_mercury;
// Compare system requirement vs pump capacity
if (h_mercury_as_water > h_pump_rating) {
std::cout << "Pump is undersized!\n"; // This will trigger!
}
```
**Problem**: Boost.Units checks dimensional compatibility (both are lengths), but cannot
distinguish between physically incompatible types of length.
=== "Pint (Python)"
```python
import pint
ureg = pint.UnitRegistry()
h_mercury = 2.0 * ureg.meter
h_pump_rating = 10.0 * ureg.meter
sg_mercury = 13.6
# Direct addition - works but physically wrong!
total_head = h_mercury + h_pump_rating # WRONG!
# Both have dimension [length], so Pint allows this
# Correct calculation still requires manual tracking:
h_mercury_as_water = h_mercury * sg_mercury
# Compare system requirement vs pump capacity
if h_mercury_as_water > h_pump_rating:
print("Pump is undersized!") # This will trigger!
```
**Problem**: Pint prevents dimensional errors but cannot distinguish between different
physical meanings of the same dimension.
The fundamental limitation: **Units libraries check dimensional compatibility
(_length_ + _length_ = OK), but cannot enforce that quantities with the same dimension
may represent incompatible physical concepts.** This is where **mp-units** breaks new
ground with its `is_kind` feature.
**Problems common to all these approaches:**
1. **No distinction**: Both _fluid head_ and _water head_ have the same dimensional type
(_length_), making them indistinguishable to the type system
2. **Silent errors**: Adding incompatible _head_ types compiles successfully but produces
physically nonsense results
3. **Manual tracking**: Programmers must remember which variables represent which type
of _head_—the type system provides no help
4. **Comparison confusion**: `2 m < 10 m` numerically, but `2 m` of mercury represents
far more _energy_ (and _pressure_) than `10 m` of water
5. **Easy to forget**: Forgetting the SG conversion factor leads to severely undersized
equipment—a potentially catastrophic error in chemical plants
**Real-world scenario:**
A chemical plant pump system must:
- Handle mercury (SG = 13.6) from a 2 m column in a reactor vessel
- Verify a pump rated for 10 m _water head_ can handle this load
- Convert the mercury _fluid head_ to equivalent _water head_ for comparison
- Prevent accidentally mixing _fluid head_ and _water head_ values
- Require explicit conversion through _specific gravity_
**The challenge:** Both are _heights_ (dimension: _length_), but they're physically
incompatible without conversion through _specific gravity_.
## Your task
Implement a type-safe hydraulic head calculation system using **mp-units** that prevents
mixing fluid head and water head without explicit conversion.
Create:
1. **Distinct kinds**: Define `fluid_head` and `water_head` as separate kinds derived from
`isq::height`
2. **Specific gravity type**: Define `specific_gravity` as a dimensionless `quantity_spec`
3. **Conversion functions**: Implement type-safe conversions between the two head types:
- `to_water_head(h_fluid, sg)` — converts _fluid head_ to _water head_ using SG
- `to_fluid_head(h_water, sg)` — converts _water head_ to _fluid head_ using SG
The solution should:
- Prevent direct addition or comparison of _fluid head_ and _water head_ (compile-time error)
- Require explicit conversion through _specific gravity_
- Use `QuantityOf` constraints for type safety
- Work with any units of _length_ (meters, feet, etc.)
```cpp
// ce-embed height=800 compiler=clang2110 flags="-std=c++23 -stdlib=libc++ -O3" mp-units=trunk
#include <mp-units/systems/si.h>
#include <iostream>
using namespace mp_units;
// TODO: Define fluid_head as a distinct kind derived from isq::height
// TODO: Define water_head as a distinct kind derived from isq::height
// TODO: Define specific_gravity as a dimensionless quantity_spec
// TODO: Implement to_water_head conversion function
// Formula: H_water = H_fluid * SG
// Hint: Return type should be QuantityOf<water_head> auto
// TODO: Implement to_fluid_head conversion function
// Formula: H_fluid = H_water / SG
int main()
{
using namespace si::unit_symbols;
// Scenario: Chemical reactor with 2m mercury column (SG = 13.6)
quantity h_mercury = fluid_head(2 * m);
quantity sg_mercury = specific_gravity(13.6 * one);
// Pump rated for 10m water head
quantity h_pump_rating = water_head(10 * m);
std::cout << "Pump System Design Analysis\n";
std::cout << "============================\n\n";
std::cout << "Mercury column height: " << h_mercury << "\n";
std::cout << "Mercury specific gravity: " << sg_mercury << "\n";
std::cout << "Pump rating (water head): " << h_pump_rating << "\n\n";
// Safety check: This should NOT compile!
// quantity wrong = h_mercury + h_pump_rating; // Error: cannot mix kinds
// Convert mercury fluid head to equivalent water head
quantity h_mercury_as_water = to_water_head(h_mercury, sg_mercury);
std::cout << "Mercury equivalent (water head): " << h_mercury_as_water << "\n\n";
// Verify pump capacity against system requirement
if (h_mercury_as_water > h_pump_rating) {
std::cout << "WARNING: System requirement (" << h_mercury_as_water
<< ") exceeds pump rating (" << h_pump_rating << ")!\n";
std::cout << "Pump is UNDERSIZED for this application.\n";
}
else {
quantity excess_capacity = h_pump_rating - h_mercury_as_water;
std::cout << "Pump capacity is adequate.\n";
std::cout << "Excess capacity: " << excess_capacity << "\n";
}
// Demonstrate reverse conversion
quantity h_back_to_fluid = to_fluid_head(h_mercury_as_water, sg_mercury);
std::cout << "\nVerification - converted back: " << h_back_to_fluid << "\n";
}
```
??? "Solution"
```cpp
#include <mp-units/systems/si.h>
#include <iostream>
using namespace mp_units;
// 1. Define the distinct kinds (The Safety Layer)
inline constexpr struct fluid_head final : quantity_spec<isq::height, is_kind> {} fluid_head;
inline constexpr struct water_head final : quantity_spec<isq::height, is_kind> {} water_head;
// 2. Define a type for Specific Gravity (Dimensionless)
inline constexpr struct specific_gravity final : quantity_spec<dimensionless> {} specific_gravity;
// 3. Define Conversion Helpers
// Formula: H_water = H_fluid * SG
constexpr QuantityOf<water_head> auto to_water_head(QuantityOf<fluid_head> auto h_fluid,
QuantityOf<specific_gravity> auto sg)
{
// We explicitly cast the result to water_head because we know the physics is correct
return water_head(isq::height(h_fluid) * sg);
}
// Formula: H_fluid = H_water / SG
constexpr QuantityOf<fluid_head> auto to_fluid_head(QuantityOf<water_head> auto h_water,
QuantityOf<specific_gravity> auto sg)
{
return fluid_head(isq::height(h_water) / sg);
}
int main()
{
using namespace si::unit_symbols;
// Scenario: Chemical reactor with 2m mercury column (SG = 13.6)
quantity h_mercury = fluid_head(2 * m);
quantity sg_mercury = specific_gravity(13.6 * one);
// Pump rated for 10m water head
quantity h_pump_rating = water_head(10 * m);
std::cout << "Pump System Design Analysis\n";
std::cout << "============================\n\n";
std::cout << "Mercury column height: " << h_mercury << "\n";
std::cout << "Mercury specific gravity: " << sg_mercury << "\n";
std::cout << "Pump rating (water head): " << h_pump_rating << "\n\n";
// Safety check: This would NOT compile!
// quantity wrong = h_mercury + h_pump_rating; // Error: cannot mix kinds
// Convert mercury fluid head to equivalent water head
quantity h_mercury_as_water = to_water_head(h_mercury, sg_mercury);
std::cout << "Mercury equivalent (water head): " << h_mercury_as_water << "\n\n";
// Verify pump capacity against system requirement
if (h_mercury_as_water > h_pump_rating) {
std::cout << "WARNING: System requirement (" << h_mercury_as_water
<< ") exceeds pump rating (" << h_pump_rating << ")!\n";
std::cout << "Pump is UNDERSIZED for this application.\n";
}
else {
quantity excess_capacity = h_pump_rating - h_mercury_as_water;
std::cout << "Pump capacity is adequate.\n";
std::cout << "Excess capacity: " << excess_capacity << "\n";
}
// Demonstrate reverse conversion
quantity h_back_to_fluid = to_fluid_head(h_mercury_as_water, sg_mercury);
std::cout << "\nVerification - converted back: " << h_back_to_fluid << "\n";
}
```
**How the solution works:**
By marking `fluid_head` and `water_head` with `is_kind`, we create distinct quantity types
that cannot be mixed despite sharing the `length` dimension:
- **Compile-time prevention**: Direct addition, comparison, or assignment between fluid head
and water head results in a compile error
- **Explicit conversion required**: The `to_water_head` and `to_fluid_head` functions
perform the physics-based conversion through specific gravity, making the conversion
visible and intentional in the code
- **Type safety at boundaries**: Functions accepting `QuantityOf<fluid_head>` or
`QuantityOf<water_head>` cannot accidentally receive the wrong type
- **Base quantity access**: When needed, both can be converted to `isq::height` using
`isq::height(h)`, allowing generic height operations while preserving type safety
at domain boundaries
This pattern is similar to how mp-units prevents mixing plane angles and solid angles—
both dimensionless quantities that share the same dimension but represent fundamentally
different physical concepts that cannot be meaningfully combined.
## References
- [User's Guide: Systems of Quantities](../users_guide/framework_basics/systems_of_quantities.md)
- [GitHub Discussion #757: Hydraulic Head](https://github.com/mpusz/mp-units/discussions/757)
## Takeaways
- **`is_kind` creates incompatible types**: Even when quantities share the same dimension,
`is_kind` prevents mixing them without explicit conversion
- **Domain-specific safety**: Hydraulic engineering's distinction between _energy_ measurements
in different reference frames (_fluid head_ vs _water head_) becomes a compile-time guarantee
- **Explicit conversions**: Physics-based conversions (through _specific gravity_) are visible
and required in the code
- **Prevents subtle bugs**: The classic mistake of treating 2 m of mercury as 2 m of water
becomes a compile error
- **Type system as documentation**: The code itself documents that these are different
physical concepts requiring conversion
- **Similar to built-in protections**: Just as **mp-units** prevents mixing radians and
steradians (_angular measure_ vs _solid angular measure_), your domain can have custom
protections
- **Explicit base conversion when needed**: Both can convert to generic `isq::height` using
`isq::height(h)` for algorithms that work on any _length_—but this requires an explicit
conversion call; implicit conversion will fail to compile, preserving type safety at
domain boundaries
- **Real-world safety**: Equipment undersizing due to _head_ calculation errors can be
catastrophic in chemical plants—type safety prevents this
- **Pattern for other domains**: This technique applies anywhere quantities share dimensions
but represent incompatible concepts—particularly _energy_ or _power_ measurements in
different reference frames (e.g., _gauge_ vs _absolute pressure_, _RMS_ vs _peak voltage_,
_true_ vs _apparent power_)