Files
mp-units/docs/tutorials/tutorial_11.md

320 lines
13 KiB
Markdown
Raw Normal View History

2026-01-05 19:17:16 +01:00
# Tutorial 11: Faster-than-Lightspeed Constants
Physical constants like standard gravity (g₀) or π often appear in calculations where
they multiply in one place and divide in another. Traditional libraries implement these
as constant values (e.g., `9.80665`), requiring runtime floating-point arithmetic even
when the constants mathematically cancel out.
This tutorial demonstrates how **mp-units** implements constants as compile-time units,
enabling automatic simplification when constants cancel, preserving exact arithmetic where
possible, and delaying expensive conversions until necessary.
## Problem statement
Consider rocket stage burn time calculations for mission planning and flight software.
Rocket engineers face a messy mix of units:
- **_Propellant mass_** in kilograms (kg)
- **_Specific impulse_ (Isp)** in seconds - the industry-standard efficiency metric
- **_Thrust_** often specified in **kilogram-force (kgf)** in legacy engine datasheets
- **_Burn time_** needs to be calculated from these parameters
The relationship between _thrust_ (F), _mass flow rate_ ($\dot{m}$), and
_specific impulse_ ($I_{sp}$) involves standard gravity ($g_0$):
$$F = \dot{m} \cdot I_{sp} \cdot g_0$$
Rearranging to find _burn time_ ($t_{burn} = m_{prop} / \dot{m}$), we get:
$$t_{burn} = \frac{m_{prop}}{\dot{m}} = \frac{m_{prop} \cdot I_{sp} \cdot g_0}{F}$$
Here's a traditional implementation with helper functions but no type safety:
```cpp
const double g0 = 9.80665; // m/s² - magic number!
double mass_flow_rate_kg_s(double thrust_N, double isp_s) { return thrust_N / (isp_s * g0); }
double burn_time_from_flow_s(double mass_kg, double flow_rate_kg_s) { return mass_kg / flow_rate_kg_s; }
int main()
{
double propellant_kg = 15000.0;
double isp_s = 311.0;
double thrust_kgf = 390000.0; // Legacy engine spec in kgf!
double thrust_N = thrust_kgf * g0;
double flow_kg_s = mass_flow_rate_kg_s(thrust_N, isp_s);
double time_s = burn_time_from_flow_s(propellant_kg, flow_kg_s);
// Can you spot a missed optimization opportunity?
}
```
**Problems with this approach:**
1. **Missed optimization**: Converting `thrust_kgf * g0` to `thrust_N`, then dividing by
`g0` in `mass_flow_rate_kg_s` — the `g0` factors cancel but we compute them anyway!
2. **No type safety**: Nothing prevents passing `thrust_kgf` directly to
`mass_flow_rate_kg_s`, silently producing wrong results
3. **Hidden unit conversions**: kgf is kg × g₀ by definition, but this relationship is
implicit in the code
4. **Manual burden**: Programmers must track which conversions are needed and which
constants cancel
5. **Historic disasters**: Unit confusion (mixing lbf and N) has caused real mission
failures like Mars Climate Orbiter
**Real-world scenario:**
Flight software for a rocket stage must:
- Read engine specifications with _thrust_ in kgf (e.g., legacy Russian engines like RD-180)
- Read _propellant mass_ from fuel tank sensors (in kg)
- Use published _Isp_ values (efficiency metric in seconds)
- Calculate _burn time_ to determine how long the stage will fire
- Handle mixed units safely, as some engines use kgf while others use N
The g₀ constant appears in:
- The definition of kgf (kilogram-force = kg × g₀)
- The _burn time_ formula (numerator has g₀)
- These should cancel perfectly, avoiding any 9.80665 multiplications
## Your task
Refactor the rocket burn time calculator to use **mp-units** with g₀ as a compile-time
constant unit. The helper functions are already provided with type-safe `QuantityOf`
constraints.
Complete the implementation by defining:
1. **standard_gravity** (g₀) — Define `standard_gravity` as a unit which represents
9.80665 m/s² as an exact rational 980'665/100'000 (or use predefined `si::standard_gravity`)
2. **kilogram_force** (kgf) — Define as a `named_unit` that embeds g₀: `kg × g₀`
With these definitions in place, observe how:
- **Type safety**: `QuantityOf` constraints prevent passing wrong unit types
- **Automatic optimization**: When thrust is in kgf, g₀ factors cancel at compile-time
without runtime cost
- **Mixed units**: Both kgf (legacy) and N (modern) thrust specifications work seamlessly
```cpp
// ce-embed height=900 compiler=clang2110 flags="-std=c++23 -stdlib=libc++ -O3" mp-units=trunk
#include <mp-units/systems/si.h>
#include <mp-units/systems/isq.h>
#include <iostream>
using namespace mp_units;
inline constexpr struct specific_impulse final :
quantity_spec<isq::time, isq::force / (isq::mass_flow_rate * isq::acceleration)> {} specific_impulse;
// TODO: Define standard gravity (g₀) as a unit (9.80665 m/s² = 980'665/100'000 m/s²)
// TODO: Define kilogram-force (kgf) as kg × g₀
inline constexpr Unit auto g0 = standard_gravity;
inline constexpr Unit auto kgf = kilogram_force;
QuantityOf<isq::mass_flow_rate> auto mass_flow_rate(QuantityOf<isq::force> auto thrust,
QuantityOf<specific_impulse> auto isp)
{
return thrust / (isp * g0);
}
QuantityOf<isq::time> auto burn_time_from_flow(QuantityOf<isq::mass> auto propellant_mass,
QuantityOf<isq::mass_flow_rate> auto flow_rate)
{
return propellant_mass / flow_rate;
}
QuantityOf<isq::speed> auto exhaust_velocity(QuantityOf<specific_impulse> auto isp)
{
return isp * g0;
}
QuantityOf<isq::time> auto burn_time_with_stats(QuantityOf<isq::mass> auto propellant_mass,
QuantityOf<specific_impulse> auto isp,
QuantityOf<isq::force> auto thrust)
{
quantity flow_rate = mass_flow_rate(thrust, isp);
quantity burn_time = burn_time_from_flow(propellant_mass, flow_rate);
quantity ve = exhaust_velocity(isp);
using namespace si::unit_symbols;
std::cout << " Isp: " << isp << "\n";
std::cout << " Thrust: " << thrust << " = " << thrust.template in<double>(kN) << "\n";
std::cout << " Exhaust velocity: " << ve << " = " << ve.in(m / s) << "\n";
std::cout << " Mass flow rate: " << flow_rate << " = " << flow_rate.in(kg / s) << "\n";
std::cout << " Burn time: " << burn_time << " = " << burn_time.in(s) << "\n\n";
return burn_time;
}
int main()
{
using namespace si::unit_symbols;
quantity propellant_mass = 15'000. * kg;
2026-01-05 20:19:32 +01:00
2026-01-05 19:17:16 +01:00
std::cout << "Rocket Stage Burn Time Analysis\n";
std::cout << "================================\n";
std::cout << "Propellant mass: " << propellant_mass << "\n\n";
// Engine 1: RD-180 (legacy Russian engine with thrust in kgf)
std::cout << "Engine 1 (RD-180):\n";
quantity engine1_isp = 311. * s;
quantity engine1_thrust = 390'000 * kgf; // Legacy unit!
quantity burn_time_1 = burn_time_with_stats(propellant_mass, engine1_isp, engine1_thrust);
// Engine 2: Modern high-efficiency engine with thrust in Newtons
std::cout << "Engine 2 (Modern high-efficiency):\n";
quantity engine2_isp = 450. * s;
quantity engine2_thrust = 500. * kN; // Modern SI unit
quantity burn_time_2 = burn_time_with_stats(propellant_mass, engine2_isp, engine2_thrust);
// Compare burn times
quantity time_ratio = burn_time_2 / burn_time_1;
std::cout << "Engine 2 burns " << time_ratio.in(one) << " times longer\n\n";
2026-01-05 20:19:32 +01:00
2026-01-05 19:17:16 +01:00
// Calculate total impulse - demonstrates seamless mixing of kgf and N
quantity total_impulse_1 = engine1_thrust * burn_time_1;
quantity total_impulse_2 = engine2_thrust * burn_time_2;
2026-01-05 20:19:32 +01:00
2026-01-05 19:17:16 +01:00
std::cout << "Total Impulse:\n";
std::cout << " Engine 1: " << total_impulse_1 << " = " << total_impulse_1.in(kN * s) << "\n";
std::cout << " Engine 2: " << total_impulse_2 << " = " << total_impulse_2.in(kN * s) << "\n";
}
```
??? "Solution"
```cpp
#include <mp-units/systems/si.h>
#include <mp-units/systems/isq.h>
#include <iostream>
using namespace mp_units;
inline constexpr struct specific_impulse final :
quantity_spec<isq::time, isq::force / (isq::mass_flow_rate * isq::acceleration)> {} specific_impulse;
inline constexpr struct standard_gravity final :
named_unit<symbol_text{u8"g₀", "g_0"}, mag_ratio<980'665, 100'000> * si::metre / square(si::second)> {} standard_gravity;
inline constexpr struct kilogram_force final :
named_unit<"kgf", si::kilogram * standard_gravity> {} kilogram_force;
inline constexpr Unit auto kgf = kilogram_force;
inline constexpr Unit auto g0 = standard_gravity;
QuantityOf<isq::mass_flow_rate> auto mass_flow_rate(QuantityOf<isq::force> auto thrust,
QuantityOf<specific_impulse> auto isp)
{
return thrust / (isp * g0);
}
QuantityOf<isq::time> auto burn_time_from_flow(QuantityOf<isq::mass> auto propellant_mass,
QuantityOf<isq::mass_flow_rate> auto flow_rate)
{
return propellant_mass / flow_rate;
}
QuantityOf<isq::speed> auto exhaust_velocity(QuantityOf<specific_impulse> auto isp)
{
return isp * g0;
}
QuantityOf<isq::time> auto burn_time_with_stats(QuantityOf<isq::mass> auto propellant_mass,
QuantityOf<specific_impulse> auto isp,
QuantityOf<isq::force> auto thrust)
{
quantity flow_rate = mass_flow_rate(thrust, isp);
quantity burn_time = burn_time_from_flow(propellant_mass, flow_rate);
quantity ve = exhaust_velocity(isp);
using namespace si::unit_symbols;
std::cout << " Isp: " << isp << "\n";
std::cout << " Thrust: " << thrust << " = " << thrust.template in<double>(kN) << "\n";
std::cout << " Exhaust velocity: " << ve << " = " << ve.in(m / s) << "\n";
std::cout << " Mass flow rate: " << flow_rate << " = " << flow_rate.in(kg / s) << "\n";
std::cout << " Burn time: " << burn_time << " = " << burn_time.in(s) << "\n\n";
return burn_time;
}
int main()
{
using namespace si::unit_symbols;
quantity propellant_mass = 15'000. * kg;
2026-01-05 20:19:32 +01:00
2026-01-05 19:17:16 +01:00
std::cout << "Rocket Stage Burn Time Analysis\n";
std::cout << "================================\n";
std::cout << "Propellant mass: " << propellant_mass << "\n\n";
// Engine 1: RD-180 (legacy Russian engine with thrust in kgf)
std::cout << "Engine 1 (RD-180):\n";
quantity engine1_isp = 311. * s;
quantity engine1_thrust = 390'000 * kgf;
quantity burn_time_1 = burn_time_with_stats(propellant_mass, engine1_isp, engine1_thrust);
// Engine 2: Modern high-efficiency engine with thrust in Newtons
std::cout << "Engine 2 (Modern high-efficiency):\n";
quantity engine2_isp = 450. * s;
quantity engine2_thrust = 500. * kN;
quantity burn_time_2 = burn_time_with_stats(propellant_mass, engine2_isp, engine2_thrust);
// Compare burn times
quantity time_ratio = burn_time_2 / burn_time_1;
std::cout << "Engine 2 burns " << time_ratio.in(one) << " times longer\n\n";
2026-01-05 20:19:32 +01:00
2026-01-05 19:17:16 +01:00
// Calculate total impulse - demonstrates seamless mixing of kgf and N
quantity total_impulse_1 = engine1_thrust * burn_time_1;
quantity total_impulse_2 = engine2_thrust * burn_time_2;
2026-01-05 20:19:32 +01:00
2026-01-05 19:17:16 +01:00
std::cout << "Total Impulse:\n";
std::cout << " Engine 1: " << total_impulse_1 << " = " << total_impulse_1.in(kN * s) << "\n";
std::cout << " Engine 2: " << total_impulse_2 << " = " << total_impulse_2.in(kN * s) << "\n";
}
```
The solution defines `kilogram_force` as `kg × standard_gravity`, embedding g₀ directly
into the unit definition. This enables elegant compile-time optimization:
- **For Engine 1 (thrust in kgf)**: The _burn time_ formula contains g₀ in the numerator,
while kgf contains g₀ in its denominator. These factors cancel perfectly at compile-time,
eliminating any runtime multiplication or division by 9.80665.
2026-01-05 20:19:32 +01:00
2026-01-05 19:17:16 +01:00
- **For Engine 2 (thrust in N)**: The g₀ factors don't cancel, but **mp-units** automatically
handles the conversion, applying g₀ exactly where needed without manual intervention.
2026-01-05 20:19:32 +01:00
2026-01-05 19:17:16 +01:00
Both engines use identical code—the type system ensures correctness regardless of whether
_thrust_ is specified in legacy (kgf) or modern (N) units.
## References
- [User's Guide: Faster-than-Lightspeed Constants](../users_guide/framework_basics/faster_than_lightspeed_constants.md)
## Takeaways
- **Constants as units**: Physical constants like g₀ become part of the type system rather
than runtime values
- **Automatic cancellation**: When constants appear in both numerator and denominator,
they cancel at compile-time without manual intervention
- **Legacy unit support**: kgf (kilogram-force) embeds g₀ in its definition, enabling
seamless calculations with historical engine specifications
- **Eliminates magic numbers**: No more manual `9.80665` conversions scattered throughout
code
- **Perfect mathematical cancellation**: The formula t = (m × Isp × g₀) / F simplifies
automatically when F is in kgf
- **Type safety**: The type system prevents mixing kg and kgf incorrectly
- **Mixed unit support**: Modern (N) and legacy (kgf) _thrust_ specifications work
together in the same codebase
- **Historical significance**: Prevents the type of unit confusion that caused real
mission failures (Mars Climate Orbiter)
- **Formula generality**: The same _burn time_ calculation works correctly regardless
of whether _thrust_ is specified in kgf or N