Files
mp-units/docs/tutorials/tutorial_11.md
2026-01-05 20:19:32 +01:00

320 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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;
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";
// 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;
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;
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";
// 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;
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.
- **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.
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