Skip to content

12. Series

A strength test is not always one specimen crushed once. One mix is often tested at several ages, 7, 14 and 28 days, and the method then states the same quantity at each age. This chapter declares the strengths at the three ages as one series, calculates each as a share of the 28-day strength element by element, and shows what happens when one age was not recorded.

The program is self-contained: it declares its own quantities and does not build on the previous chapters' programs.

One quantity at several points

The strength at each age is one quantity, the compressive strength, with a value at each of three points. The shares are taken of the 28-day strength, the series' third element, but a formula cannot read one element of a series. So the 28-day strength is also entered on its own, as f_c28, and must be the same value as the series' third element. Nothing checks the two against each other, so keep them in step. The share is a pure number:

using Strength = formula::Quantity<struct StrengthTag, "f_c", "compressive strength at an age", unit::Megapascal>;
using FinalStrength =
    formula::Quantity<struct FinalStrengthTag, "f_c28", "compressive strength at 28 days", unit::Megapascal>;
using Share = formula::Quantity<struct ShareTag, "s", "share of the 28-day strength", unit::One>;

formula::series<Q, N> is the quantity Q at N points. The length is part of the type, because the ages are part of the method, not data: an age the laboratory did not record is an absent element, never a shorter series. A series is not a single value, and a series in a single value's place does not compile.

The share of each age's strength in the 28-day strength divides the series by the single value:

constexpr auto shareOfFinal = formula::yields<Share>(formula::series<Strength, 3> / var<FinalStrength>);

Rendered, the series variable is marked with (i), and the single value is not:

std::println("{} = {}", formula::symbol_of<Share>(), formula::render(shareOfFinal));

Arithmetic element by element

Arithmetic on a series is applied to each element: here each of the three strengths is divided by the 28-day strength, which is read once and used for every element. formula::explain_series evaluates a series and records how, as formula::checked_explain does for a single value. Its outcome is a std::expected: the series, or a formula::SeriesFailure that names the element that failed when the failure belongs to one. outcome->element(at) is one element, at a zero-based position:

/// The ages the strengths are taken at, in days, one per element.
constexpr std::array<int, 3> ages { 7, 14, 28 };

/// Prints how each share of @p specimens was calculated, then each share
/// rounded for reading. False when the series failed.
template <typename Env>
bool report(Env const& specimens)
{
    auto const shares = formula::explain_series(shareOfFinal, specimens);
    std::print("{}", formula::render_trace(shares.trace, { .maxSteps = 10 }));
    if (!shares.outcome)
    {
        std::println("no shares: {}", shares.outcome.error().error);
        return false;
    }
    for (std::size_t at = 0; at < ages.size(); ++at)
        std::println("{} days: {} = {:~.3HalfEven}", ages[at], formula::symbol_of<Share>(), shares.outcome->element(at));
    return true;
}

The strengths 21, 26 and 30 MPa are measured at the three ages, and the 28-day strength is 30 MPa:

auto const recorded =
    formula::environment(formula::measured_series<Strength>(21, 26, 30), formula::Measured<FinalStrength> { 30 });
std::println("all three recorded:");
if (!report(recorded))
    return 1;

The trace records one step for the whole division, with every element's result: 21/30 is 7/10, 26/30 is 13/15, and 30/30 is 1. The shares are pure numbers, so the trace shows them without a unit. Rounded for reading, 7/10 is the exact decimal 0.7 and 1 is 1; 13/15 has no exact decimal, so it is rounded to three places and marked: ≈0.867.

An absent element stays absent

formula::not_measured in place of a value marks one element absent: here the 14-day strength was not recorded:

auto const oneUnrecorded = formula::environment(formula::measured_series<Strength>(21, formula::not_measured, 30),
                                                formula::Measured<FinalStrength> { 30 });
std::println("the 14-day strength not recorded:");
if (!report(oneUnrecorded))
    return 1;

The share at 14 days is absent too, and printed (not measured). The shares at 7 and 28 days are still calculated, and are the same as before. An absent element makes absent exactly the elements that depend on it, and the division of one element depends on no other. A step that combines the elements into one value, such as their sum, is absent as a whole when any element is: a total of only the elements someone entered is not the total the method states.

Output

s = f_c(i) / f_c28
all three recorded:
1. f_c = 21 MPa; 26 MPa; 30 MPa
2. f_c28 = 30 MPa
3. #1 / #2 = 7/10; 13/15; 1
7 days: s = 0.7
14 days: s = ≈0.867
28 days: s = 1
the 14-day strength not recorded:
1. f_c = 21 MPa; (not measured); 30 MPa
2. f_c28 = 30 MPa
3. #1 / #2 = 7/10; (not measured); 1
7 days: s = 0.7
14 days: s = (not measured)
28 days: s = 1

Summary

  • formula::series<Q, N> -- the quantity Q at N points, written (i) when rendered.
  • formula::measured_series<Q>(values...) -- the measured values of a series, one per point; formula::not_measured marks one absent.
  • formula::explain_series(formula, environment) -- evaluates a series and records how; outcome->element(at) is one element.

Further reading