Skip to content

15. Opaque operations and bounded retry

Two things a strength test method states are not a formula over its inputs. It may name a computation without spelling it out, such as the stiffness of the loading as the slope of a straight line fitted by least squares through the load and displacement readings. And it may repeat a step until it is accepted, a bounded number of times, such as a retest repeated until two results agree. This chapter fits that line and runs that retest, and shows what the trace says about each.

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

A named operation whose inside is not traced

The readings are a displacement in mm and a load in kN. The fit's outputs are quantities of their own: the stiffness, the slope of the line, in kN/mm; the load at zero displacement, its intercept, in kN; and the coefficient of determination, R², a pure number. The library has no kN/mm, so the program declares it, as one kN/mm is 1000000 N/m:

/// Kilonewtons per millimetre: one is 1000000 N/m.
constexpr formula::Unit kilonewtonPerMillimetre { .dimension = formula::dim::ForcePerLength,
                                                  .magnitudeNumerator = 1'000'000,
                                                  .symbolText = formula::symbol("kN/mm"),
                                                  .decimals = 1 };

using Displacement = formula::Quantity<struct DisplacementTag, "s", "displacement", unit::Millimetre>;
using Load = formula::Quantity<struct LoadTag, "F", "load", unit::Kilonewton>;
using Stiffness = formula::Quantity<struct StiffnessTag, "k", "stiffness", kilonewtonPerMillimetre>;
using LoadAtZero = formula::Quantity<struct LoadAtZeroTag, "F_0", "load at zero displacement", unit::Kilonewton>;
using FitQuality = formula::Quantity<struct FitQualityTag, "R2", "coefficient of determination", unit::One>;

formula::linear_least_squares is an opaque operation: the call names it, a citation and its inputs, and the trace shows its inputs and outputs but not the sums inside. Its inputs here are formula::observations<Q, Capacity>, as many readings as were made, up to the capacity, so how many points there are is data. formula::opaque_output<"name"> chooses one output, intercept, slope, r squared or points, and a formula uses it like any other value:

constexpr auto fit =
    formula::linear_least_squares(formula::observations<Displacement, 8>,
                                  formula::observations<Load, 8>,
                                  { .title = "Stiffness", .reference = "Example Standard 12:2020", .section = "5.2" });

constexpr auto stiffness = formula::yields<Stiffness>(formula::opaque_output<"slope">(fit));
constexpr auto loadAtZero = formula::yields<LoadAtZero>(formula::opaque_output<"intercept">(fit));
constexpr auto fitQuality = formula::yields<FitQuality>(formula::opaque_output<"r squared">(fit));

The citation has no default: the reference that defines the operation is what a reader has instead of its inside.

Four invented readings lie exactly on the line load = 100 kN/mm × displacement + 5 kN: (0.1 mm, 15 kN), (0.2 mm, 25 kN), (0.3 mm, 35 kN) and (0.4 mm, 45 kN). The program renders the stiffness, explains it, and prints it:

auto const readings = formula::environment(formula::MeasuredObservations<Displacement, 8>(0.1_r, 0.2_r, 0.3_r, 0.4_r),
                                           formula::MeasuredObservations<Load, 8>(15_r, 25_r, 35_r, 45_r));

std::println("{} = {}", formula::symbol_of<Stiffness>(), formula::render(stiffness));
auto const explained = formula::checked_explain(stiffness, readings);
if (!explained)
{
    std::println("no stiffness: {}", explained.error().error);
    return 1;
}
std::print("{}", formula::render_trace(explained->trace, { .maxSteps = 20 }));
std::println("{} = {}", formula::symbol_of<Stiffness>(), explained->outcome);

Lines 1 and 2 of the trace are the readings, exact, so 0.1 mm is 1/10 mm. Line 3 is the operation: every output it produced, then [inside not shown], then its citation. The library writes [inside not shown] for every opaque call, rather than leave a reader to wonder whether a step is missing. Line 4 chooses the slope. The fit is exact: the means are 0.25 mm and 30 kN, the sum of the squared displacements about their mean is 0.05 mm², and the sum of the products about the means 5 kN·mm, so the slope is 5/0.05 = 100 kN/mm and the intercept 30 kN less 100 kN/mm × 0.25 mm, 5 kN. Every point lies on the line, so R² is 1.

The other two outputs are evaluated the same way:

auto const atZero = formula::checked_evaluate(loadAtZero, readings);
if (!atZero)
{
    std::println("no load at zero displacement: {}", atZero.error());
    return 1;
}
auto const quality = formula::checked_evaluate(fitQuality, readings);
if (!quality)
{
    std::println("no coefficient of determination: {}", quality.error());
    return 1;
}
std::println("{} = {}", formula::symbol_of<LoadAtZero>(), *atZero);
std::println("{} = {}", formula::symbol_of<FitQuality>(), *quality);

Each output is its own value, so each evaluation runs the fit again; the operation is pure, so every run gives the same line.

A step repeated a bounded number of times

An invented rule retests a specimen at most 3 times, until two consecutive results differ by at most 0.5 MPa; if none do, the method's verdict is "test further specimens". formula::retry<R, Max, FirstJudged>(attempt, acceptance, verdict, citation) states it:

using Retest = formula::Quantity<struct RetestTag, "f_t", "strength of one retest", unit::Megapascal>;
using AgreedStrength = formula::Quantity<struct AgreedStrengthTag, "f_a", "agreed strength", unit::Megapascal>;

constexpr auto agree = formula::abs(formula::this_attempt<AgreedStrength> - formula::previous_attempt<AgreedStrength>)
                       <= formula::constant<unit::Megapascal>(0.5_r);

constexpr auto retest = formula::retry<AgreedStrength, 3, formula::FirstJudged::AtSecondAttempt>(
    formula::attempt_input<Retest>,
    agree,
    formula::Verdict { "test further specimens" },
    { .title = "Agreed strength", .reference = "Example Standard 12:2020", .section = "6" });
  • formula::attempt_input<Retest> is the attempt: at attempt k, the kth element of a series of retest results, one per attempt allowed.
  • formula::this_attempt<AgreedStrength> is the value just produced, and formula::previous_attempt<AgreedStrength> the one before; the acceptance compares the two with formula::abs.
  • 3 is the most attempts: a template argument, so no run makes more.
  • formula::FirstJudged::AtSecondAttempt judges from the second attempt, since at the first there is only one result to compare.

The results are 30.0, 31.2 and 31.0 MPa. formula::explain_retry runs the retry and records how; its outcome is a std::expected, the outcome or the arithmetic failure that stopped it, and is checked before it is read:

std::println("{}", formula::render(retest));
auto const results = formula::environment(formula::measured_series<Retest>(30_r, 31.2_r, 31_r));
auto const retested = formula::explain_retry(retest, results);
std::print("{}", formula::render_trace(retested.trace, { .maxSteps = 40 }));
if (!retested.outcome)
{
    std::println("the retest failed: {}", retested.outcome.error().error);
    return 1;
}
std::println("the retest: {} after {} attempt(s), {} = {}",
             retested.outcome->end(),
             retested.outcome->attempts_made(),
             formula::symbol_of<AgreedStrength>(),
             retested.outcome->outcome());

The render writes 0.5 MPa as the exact 1/2 MPa. Attempt 1 is 30 MPa and is not judged. Attempt 2, 31.2 MPa, differs from it by 1.2 MPa, more than 0.5 MPa, and is rejected. Attempt 3, 31.0 MPa, differs from 31.2 MPa by 0.2 MPa, and is accepted: the retry's value is 31 MPa.

Ending in exactly one of a fixed set of ways

A retry ends in exactly one of the six ways formula::RetryEnd names. outcome->end() reports five of them; a failed retry has no outcome, only the failure:

  • accepted: the acceptance held; the value is that attempt's, as here.
  • exhausted: the acceptance never held in the attempts allowed; the outcome is the verdict, "test further specimens", not the last result.
  • not judgeable: an attempt's value, or its judgement, was absent.
  • not recorded: a retest result an attempt needed was not recorded.
  • failed: an attempt failed arithmetically; there is no outcome, only the failure, naming the attempt.
  • manually entered: a person typed in the result; no attempt runs.

Had the third result been 30.4 MPa, 0.8 MPa from the second, the retry would have ended exhausted, with the verdict as its outcome.

Output

k = linear least squares(s(i), F(i)).slope
1. s = 1/10 mm; 1/5 mm; 3/10 mm; 2/5 mm
2. F = 15 kN; 25 kN; 35 kN; 45 kN
3. linear least squares(#1, #2) = intercept = 5 kN; slope = 100 kN/mm; r squared = 1; points = 4 [inside not shown] [Stiffness, Example Standard 12:2020, 5.2]
4. slope of #3 = 100 kN/mm
k = 100 kN/mm
F_0 = 5 kN
R2 = 1
up to 3 attempts: f_a(k) = f_t(k); accept from attempt 2 when abs(f_a(k) - f_a(k-1)) <= 1/2 MPa; otherwise: test further specimens
1. f_t(k) = 30 MPa
2. attempt 1: f_a(k) = #1 = 30 MPa; not judged
3. f_t(k) = 156/5 MPa
4. f_a(k) = 156/5 MPa
5. f_a(k-1) = 30 MPa
6. #4 - #5 = 6/5 MPa
7. abs(#6) = 6/5 MPa
8. 1/2 MPa
9. attempt 2: f_a(k) = #3 = 156/5 MPa; judged #7 <= #8: rejected
10. f_t(k) = 31 MPa
11. f_a(k) = 31 MPa
12. f_a(k-1) = 156/5 MPa
13. #11 - #12 = -1/5 MPa
14. abs(#13) = 1/5 MPa
15. 1/2 MPa
16. attempt 3: f_a(k) = #10 = 31 MPa; judged #14 <= #15: accepted
17. f_a = retry: accepted at attempt 3 of 3 = 31 MPa [Agreed strength, Example Standard 12:2020, 6]
the retest: accepted after 3 attempt(s), f_a = 31 MPa

Summary

  • formula::linear_least_squares(x, y, citation) -- a straight line fitted by least squares, an opaque operation with the outputs intercept, slope, r squared and points.
  • formula::opaque_output<"name">(call) -- one output of an opaque operation, used like any other value.
  • formula::observations<Q, Capacity> -- as many readings of Q as were made, up to Capacity.
  • formula::retry<R, Max, FirstJudged>(attempt, acceptance, verdict, citation) -- a step repeated at most Max times until acceptance holds.
  • formula::explain_retry(retry, environment) -- runs a retry and records how; outcome->end() says how it ended.

Further reading