Storage Tank Calculations with Custom Quantity Types¶
Try it live on Compiler Explorer
Overview¶
This example demonstrates how to create domain-specific quantity types with constrained quantity equations, modeling a practical engineering problem: calculating fill levels, capacities, and flow rates for storage tanks.
Key Concepts¶
Custom Quantity Specifications¶
The library allows defining specialized quantity types that are more specific than the base ISQ quantities:
// add a custom quantity type of kind isq::length
QUANTITY_SPEC(horizontal_length, isq::length);
// add a custom derived quantity type of kind isq::area
// with a constrained quantity equation
QUANTITY_SPEC(horizontal_area, isq::area, horizontal_length* isq::width);
Why constrain quantity equations?
horizontal_lengthis explicitly a kind ofisq::length, but it represents specifically horizontal measurementshorizontal_areamust be calculated ashorizontal_length * isq::width, not just any two lengths- This prevents mixing incompatible physical interpretations (e.g., vertical × vertical areas)
Engineering Calculations with Quantities¶
The StorageTank class demonstrates practical engineering calculations with full dimensional
analysis:
inline constexpr auto air_density = isq::mass_density(1.225 * kg / m3);
class StorageTank {
quantity<horizontal_area[m2]> base_;
quantity<isq::height[m]> height_;
quantity<isq::mass_density[kg / m3]> density_ = air_density;
public:
constexpr StorageTank(const quantity<horizontal_area[m2]>& base, const quantity<isq::height[m]>& height) :
base_(base), height_(height)
{
}
constexpr void set_contents_density(const quantity<isq::mass_density[kg / m3]>& density)
{
assert(density > air_density);
density_ = density;
}
[[nodiscard]] constexpr QuantityOf<isq::weight> auto filled_weight() const
{
const auto volume = isq::volume(base_ * height_); // TODO check if we can remove that cast
const QuantityOf<isq::mass> auto mass = density_ * volume;
return isq::weight(mass * g);
}
[[nodiscard]] constexpr quantity<isq::height[m]> fill_level(const quantity<isq::mass[kg]>& measured_mass) const
{
return height_ * measured_mass * g / filled_weight();
}
[[nodiscard]] constexpr quantity<isq::volume[m3]> spare_capacity(const quantity<isq::mass[kg]>& measured_mass) const
{
return (height_ - fill_level(measured_mass)) * base_;
}
};
Notice how:
filled_weight()properly multiplies volume by density and gravitational accelerationfill_level()inverts the calculation, and dimensional analysis automatically cancelsgfrom both numerator and denominator:(measured_mass × g) / (density × volume × g)→measured_mass / (density × volume)spare_capacity()computes remaining volume from geometric constraints
Polymorphic Tank Shapes¶
The example shows how object-oriented design works naturally with quantities:
class CylindricalStorageTank : public StorageTank {
public:
constexpr CylindricalStorageTank(const quantity<isq::radius[m]>& radius, const quantity<isq::height[m]>& height) :
StorageTank(quantity_cast<horizontal_area>(std::numbers::pi * pow<2>(radius)), height)
{
}
};
class RectangularStorageTank : public StorageTank {
public:
constexpr RectangularStorageTank(const quantity<horizontal_length[m]>& length, const quantity<isq::width[m]>& width,
const quantity<isq::height[m]>& height) :
StorageTank(length * width, height)
{
}
};
Different tank shapes can be modeled through inheritance while maintaining type safety and dimensional correctness.
Example Application¶
Monitoring a rectangular tank being filled with water:
{
const quantity height = isq::height(200 * mm);
auto tank = RectangularStorageTank(horizontal_length(1'000 * mm), isq::width(500 * mm), height);
tank.set_contents_density(1'000 * kg / m3);
const auto duration = std::chrono::seconds{200};
const quantity fill_time = value_cast<int>(quantity{duration}); // time since starting fill
const quantity measured_mass = 20. * kg; // measured mass at fill_time
const quantity fill_level = tank.fill_level(measured_mass);
const quantity spare_capacity = tank.spare_capacity(measured_mass);
const quantity filled_weight = tank.filled_weight();
const QuantityOf<isq::mass_change_rate> auto input_flow_rate = measured_mass / fill_time;
const QuantityOf<isq::speed> auto float_rise_rate = fill_level / fill_time;
const QuantityOf<isq::time> auto fill_time_left = (height / fill_level - 1 * one) * fill_time;
const quantity fill_ratio = fill_level / height;
std::cout << MP_UNITS_STD_FMT::format("fill height at {} = {} ({} full)\n", fill_time, fill_level,
fill_ratio.in(percent));
std::cout << MP_UNITS_STD_FMT::format("fill weight at {} = {} ({})\n", fill_time, filled_weight, filled_weight.in(N));
std::cout << MP_UNITS_STD_FMT::format("spare capacity at {} = {}\n", fill_time, spare_capacity);
std::cout << MP_UNITS_STD_FMT::format("input flow rate = {}\n", input_flow_rate);
std::cout << MP_UNITS_STD_FMT::format("float rise rate = {}\n", float_rise_rate);
std::cout << MP_UNITS_STD_FMT::format("tank full E.T.A. at current flow rate = {}\n", fill_time_left.in(s));
}
Sample Output:
fill height at 200 s = 0.04 m (20 % full)
fill weight at 200 s = 100 kg g₀ (980.665 N)
spare capacity at 200 s = 0.08 m³
input flow rate = 0.1 kg/s
float rise rate = 2e-04 m/s
tank full E.T.A. at current flow rate = 800 s
Why This Matters¶
- Domain Modeling: Custom quantity types encode domain knowledge (horizontal vs vertical measurements)
- Compile-Time Safety: Invalid quantity equations are caught at compile time
- Engineering Accuracy: Complex formulas are automatically verified for dimensional correctness
- Practical Applications: Tank monitoring, fluid management, industrial process control
This pattern is valuable for any domain where specialized quantity types improve clarity and safety.