Skip to content

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_length is explicitly a kind of isq::length, but it represents specifically horizontal measurements
  • horizontal_area must be calculated as horizontal_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 acceleration
  • fill_level() inverts the calculation, and dimensional analysis automatically cancels g from 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.