Skip to content

Lookup tables

A method's own algebra sometimes needs a number no formula computes. The procedure does not derive a size-correction factor from anything; it publishes a table and tells you to read the right row out of it. formula-cpp models that as an ordinary node, so a table sits inside an expression wherever a number of its dimension would sit, and renders, documents, evaluates and traces like everything else.

There are three kinds, and they are not interchangeable:

Kind What selects a row What you get back
Banded (banded_lookup) a measured value falls in an interval the correction that interval stores
Exact (exact_lookup) a category key names a row directly that row's correction
Interpolating (interpolating_lookup) a measured value sits between two rows the value those two rows imply there — a number in no row at all

The worked example is examples/lookup_tables.cpp; every block on this page formatted as program output is copied verbatim from that program's actual output, the same way Constraints and verdicts does for examples/constraints.cpp.

What this is for — and what it is not for

A test method is code. A product specification is data. A method's algebra is fixed: it is published once, it is the same for every customer, and it belongs in a library. The limit tables a product is judged against are master data — registered per customer, per region, per contract, changed without anyone recompiling anything. This library expresses methods, and never specification tables.

That line runs straight through the middle of a lookup node, which is why it is worth stating before anything else. A table's structure — which rows exist, the unit its keys are stated in, the unit its values are stated in — is part of the method, so it lives in the node's type, as non-type template parameters. A table's contents — the number each row actually gives — are a registered table's data, so they are an ordinary runtime member, handed to the factory:

[[nodiscard]] constexpr auto sizeFactor()
{
    return formula::banded_lookup<unit::Millimetre, SizeBands, unit::Percent>(var<Diameter>,
                                                                              { rat(95), rat(100), rat(105) });
}

Everything in the template argument list — the unit the keys are stated in, the bands themselves, the unit the values are stated in — is the method: fixed, published once, and part of what this formula is. The braced list is the registered table's data, and a caller may build the node from numbers that arrived over the wire a moment ago.

Getting a band boundary wrong is a compile error naming the offending bands. Getting a correction wrong is a data problem, and this library cannot and should not try to catch it.

A reader who misses this distinction will reach for a lookup node to hold a customer's acceptance limits. Do not: those change per contract, and a compile-time template argument is the one place they cannot live.

Banded: a measured value falls in an interval

A band table is declared as formula::BandTable<N>, each row a low bound (inclusive) and a high bound (exclusive), both as exact numerator/denominator pairs:

inline constexpr formula::BandTable<3> SizeBands {
    formula::band(0, 1, 100, 1),   // 0 to under 100 mm
    formula::band(100, 1, 150, 1), // 100 to under 150 mm
    formula::band(150, 1, 200, 1), // 150 to under 200 mm -- 200 mm itself is NOT in it
};

int64 pairs rather than Rational, for the reason Dimensions and units gives for Unit's own magnitude and offset fields: Rational keeps its members private, so it is not a structural type and cannot be a non-type template parameter. An aggregate of plain integers is, and so is a std::array of them — which is what makes it possible to validate a whole table with static_assert rather than only when it happens to be loaded at run time.

The node renders as one field per row, in the table's own declared order:

banded:        lookup(d, 0 to under 100 mm gives 95 %, 100 to under 150 mm gives 100 %, 150 to under 200 mm gives 105 %)

and looking a diameter up in it gives back that row's correction:

d = 120 mm:    1

1, exactly: 120 mm falls in the middle band, whose correction the table states as 100 %, converted into the dimensionless unit the result quantity declares. Both sides of a table are converted — the key into the unit the bands are stated in, and the value out of the unit the rows are stated in — so a table may be written in whatever units the published document uses.

Bands are half-open, and reconciling that against your source document is your job

A band runs from its low bound up to but not including its high bound. A value sitting exactly on a boundary belongs to the band whose low bound it is, never the band whose high bound it is:

d = 100 mm:    1 (the band above the boundary, never the one below)

A published table that writes one row as "30 to 40" and the next as "40 to 50" leaves the value 40 ambiguous on the page. This library resolves it one way, uniformly, rather than guessing which the author of a given table meant — and it says so here rather than leaving you to find out from a mis-bucketed specimen.

So a published row that genuinely means "up to and including the maximum" must be written with its high bound at the next tick past that maximum. Not approximately past it: a real, exact number. A physical measurement is always read to some declared decimal precision — Unit::decimals — so "the next tick" always exists. unit::Millimetre declares one decimal, so a row meaning "150 mm to 200 mm inclusive" is written with its high bound at 200.1 mm:

inline constexpr formula::BandTable<1> TopRowInclusive {
    formula::band(150, 1, 2001, 10), // 150 to under 200.1 mm -- 200 mm IS in it
};

Both spellings, evaluated at exactly 200 mm, side by side:

d = 200 mm:    argument outside the domain of the operation
inclusive top: lookup(d, 150 to under 2001/10 mm gives 105 %)
d = 200 mm:    21/20

The first table's last row stops under 200 mm, so 200 mm is in no band and there is no answer. The second reaches it.

There is deliberately no closed-upper-bound flag on Band to spare you this. Adding one would only move the ambiguity from "which bound is closed" to "is the flag set correctly on the right row", and it would give the one validation rule below two different adjacency rules to reconcile instead of one.

A table with a gap does not compile, and the message says where

A published table can contain a typo, and the two that matter are a gap (a region of the domain no row covers) and an overlap (a region two rows both claim). Both are refused at compile time.

This is not a claim about the library; it is a file in it. test/negative/lookup_band_gap.cpp declares a four-row table with a gap in its middle pair — a defect at either end is the easy case — and CI asserts both that it fails to build and that it fails for the stated reason:

    inline constexpr formula::BandTable<4> GappedTable {
        formula::band(0, 1, 10, 1),
        formula::band(10, 1, 20, 1),
        formula::band(25, 1, 35, 1), // gap: band[1]'s high (20) != band[2]'s low (25)
        formula::band(35, 1, 45, 1),
    };

    inline constexpr auto broken =
        formula::banded_lookup<formula::unit::Millimetre, GappedTable, formula::unit::One>(
            formula::var<Diameter>,
            { formula::Rational { 1 }, formula::Rational { 1 }, formula::Rational { 1 }, formula::Rational { 1 } });

Compiling it says, verbatim, on clang-cl 22.1.3 (the clangcl-debug preset's compiler), with the rest of the instantiation backtrace below these lines:

In file included from test\negative\lookup_band_gap.cpp:9:
In file included from include\formula-cpp/lookup.hpp:469:
include\formula-cpp/band.hpp(219,19): error: static assertion failed due to requirement 'bands_are_adjacent(formula::Band{10, 1, 20, 1}, formula::Band{25, 1, 35, 1})': formula: this band table has a gap or overlap between two adjacent bands; the earlier band's declared high bound and the later band's declared low bound do not match exactly, and the two offending Band values appear in this diagnostic as the template arguments First and Second of RequireBandsAdjacent
  219 |     static_assert(bands_are_adjacent(First, Second),
      |                   ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
include\formula-cpp/band.hpp(265,17): note: in instantiation of template class 'formula::RequireBandsAdjacent<Band{10, 1, 20, 1}, Band{25, 1, 35, 1}>' requested here

The message names both offending rows, as the values you typed: the one ending at 20 and the one starting at 25. You are not told "a table is invalid" and left to find which row; you are told which two.

Consider what a library that accepted this table would produce. It would compile, link, run, and answer for every diameter — including the ones from 20 mm up to 25 mm, where the method defines nothing at all. Whatever it returned there would be invented: the band below, the band above, zero, the first row. A number for an input the method never defined is worse than no number, because nothing downstream can tell it apart from one the method did define. A test report carrying it is wrong in a way no reader can see. Refusing the table at compile time is the only outcome that cannot mislead anybody.

The same check runs over every band and every adjacent pair, not just the last of either, and there is a matching band_table_is_well_formed for a table that only arrives at runtime — built on the same two predicates, so the compile-time and the runtime answer cannot drift apart.

Three more mistakes are refused the same way: a row whose low bound is not below its high bound, a zero-width row, and a corrections list of the wrong length. That last one is worth its own sentence. Corrections<N> exists precisely so that a short braced list is a compile error naming both counts — handed to a bare std::array<Rational, N>, a short list silently value-initialises the rest to zero, and a row whose correction you forgot to type would then answer 0, confidently, indistinguishable from a deliberate zero.

It is the node's own member type, not only the factory's parameter type, and that distinction was bought the hard way. A lookup node is a public aggregate with public members, so it can be declared without calling a factory at all — and while the member was a raw array, that route bypassed the check entirely and the untyped rows evaluated to 0 on all three kinds. The factory's parameter cannot see a call that never happens. A consequence worth knowing: a lookup node has no default constructor, because {} for a table of three rows is a count of zero, which is exactly the mistake being refused.

A miss is not a value

A diameter in no band. A key no row names. An age off the end of a curve. In all three cases the lookup found nothing, and this library says so — three separate lines of the example's output, one per kind, gathered here:

d = 200 mm:    argument outside the domain of the operation
DrilledCore:   argument outside the domain of the operation (a key no row of the table names)
d = 220 mm:    argument outside the domain of the operation (no extrapolation past the last row)

That is ArithmeticError::DomainError, arriving through the same channel every other arithmetic failure in this library arrives through: checked_evaluate returns a std::expected whose error it is. DomainError is not a euphemism borrowed to fit — a banded table's domain is the union of its bands, an exact table's domain is its set of keys, and an interpolating curve's domain is the span from its first row to its last, exactly and literally.

There is no default-value parameter, and there will not be one. Not zero, not the nearest band, not the first row, not the last one, and — for an interpolating table — no extrapolation past the outermost breakpoints. Every one of those is a number the published document never states, invented by this library and then handed to somebody who cannot tell it apart from a number the document did state. It is the same argument Constraints and verdicts makes about bool satisfied(): an API that must answer something for the unresolved case, where every available answer is a lie.

A method that genuinely says "hold the last value beyond the final row" is a clamp its author writes explicitly, in the formula, where a reader can see it. That is a real thing some methods say, and writing it down is a one-line when() (Rounding and conditionals). What must never happen is this library applying it silently to a method that did not say it.

The trace is where a miss stops being opaque — see below.

Exact: a category key names a row

Not every published table buckets a measurement. A method just as often publishes one row per variant — a specimen shape, an apparatus type, a curing regime — and the row is chosen by which variant is in front of you, not by how large anything is. That key is a discriminator, not a quantity: no dimension, no unit, no order. A band table cannot express it.

A key is an enumerator of a scoped enumeration you declare, and the table is a KeyTable<Key, N>:

enum class LookupExampleShape : std::uint8_t
{
    Cube = 3,
    Cylinder = 7,
    Prism = 11,
    DrilledCore = 13, // deliberately absent from ShapeKeys below -- the miss
};

inline constexpr formula::KeyTable<LookupExampleShape, 3> ShapeKeys {
    LookupExampleShape::Cube,
    LookupExampleShape::Cylinder,
    LookupExampleShape::Prism,
};

A scoped enumeration rather than a string, and the deciding question is what happens when a key is absent. Write "cylindr" where the table says "cylinder" and you get a clean compile and a formula that reports a domain error for every specimen, with nothing anywhere saying which key was wrong. Write LookupExampleShape::Cylindr and the language itself stops you at the offending token, quoting the misspelling and listing the enumerators that do exist. No library diagnostic beats that, and none is needed to get it. A key's type also names the categorisation, so two tables keyed on Shape and on Apparatus cannot be crossed — two tables keyed on strings both accept "a".

An unscoped enumeration and a plain int are both refused, because both convert to and from arithmetic silently, which gives back exactly the property the scoped enumeration was chosen for.

A repeated key is refused at compile time too, naming both offending keys. It is not harmless: the second row becomes unreachable, so a correction somebody entered is silently never selected.

The key renders as its underlying value, not the enumerator's name

exact:         lookup(key 7, key 3 gives 100 %, key 7 gives 97 %, key 11 gives 92 %)

key 7 is Cylinder, and the rendering cannot say so. A C++ enumerator has no name at run time — there is no portable way to get Cylinder back out of a LookupExampleShape — so what a reader is given is the underlying value, which is the only thing that survives. It is the value, not the row's index: an author who numbers theirs { Cube = 3, Cylinder = 7 } -- as the example above does, for exactly this reason -- sees 3 and 7, numbers that appear in their own source and nowhere in a row count.

So a reader reconciling a rendered exact lookup against a published table has to carry your enum class declaration across. key 7 says which row, not which variant. Per-row labels are deliberately not modelled on the node — a table's identity is documented()'s job, not a field smuggled into the arithmetic — so if you publish a rendering of an exact lookup, publish the enumeration next to it.

Give each translation unit's key enumeration a name of its own

A key enumeration declared in an anonymous namespace — the ordinary way to write one in a .cpp — has internal linkage, so KeyTable<Shape, N> in two such files names two different types. Clang spells an anonymous namespace _GLOBAL__N_1 with no per-translation-unit discriminator, so if two files' enumerations share a name and their tables share their element values, both template parameter objects mangle to one name, land in one COMDAT group, and the linker keeps one and points the other file's reference into a discarded section.

This is worth a paragraph because of how it fails: a link error with a dangling relocation, naming a mangled symbol and not your key type, from a program that compiled without a single diagnostic. Measured on clang 20.1.8; cl 19.51, clang-cl 22 and g++ 14.2 all link it without complaint, so it is the kind of defect that appears on one leg of a build matrix and nowhere else.

Two things end it, and both cost nothing:

  • give each translation unit's key enumeration a distinct name — this page's example calls its own LookupExampleShape rather than Shape for exactly this reason; or
  • declare the enumeration in a header, where external linkage makes it one type and one specialisation, and the question does not arise.

Where the key comes from

An ExactLookupNode has no operand. A category is not a quantity, and an Environment carries measurements and nothing else, so there is no channel through which a discriminator could arrive at evaluation time. The key is runtime state on the node, handed to the factory — which means a formula whose key varies per specimen is a function of the key:

[[nodiscard]] constexpr auto shapeFactor(LookupExampleShape shape)
{
    return formula::exact_lookup<ShapeKeys, unit::Percent>(shape, { rat(100), rat(97), rat(92) });
}

A node is a cheap aggregate, so that is one build per specimen, not one evaluation per specimen.

Interpolating: a value between two rows

A published curve states a value at a handful of key points, and a specimen measured between two of them is not "in a row": the method means the value those two rows imply at that point. A band table cannot express that, because a band stores one correction for its whole interval. This kind computes.

The table is points, not intervals — one key per row, and the interval between two rows is implied by the rows rather than declared:

inline constexpr formula::BreakpointTable<3> SizeCurve {
    formula::breakpoint(100),
    formula::breakpoint(150),
    formula::breakpoint(200),
};

so it renders as the points it is, with at rather than any interval wording:

interpolating: interpolate(d, at 100 mm gives 95 %, at 150 mm gives 100 %, at 200 mm gives 105 %)

and between two rows it produces a number that appears in neither:

d = 120 mm:    97/100 (between two rows -- in neither of them)

Every step of that is Rational's own checked arithmetic — y0 + (x - x0)(y1 - y0)/(x1 - x0) with no rounding anywhere — so a result no finite decimal can hold comes back as the exact fraction it is. The only way the answer is not exact is that some intermediate leaves Rational's representable range, and that is reported as ArithmeticError::Overflow rather than approximated away.

A table's well-formedness here is that its breakpoints strictly ascend. Two rows at one key state two values at one key and leave a zero-width segment to divide by; two rows out of order make "between these two rows" meaningless. One rule, one diagnostic, refused at compile time and naming both offending rows.

The two domains deliberately disagree at the top, and nobody should harmonise them

A band table's top bound is excluded. An interpolating table's last breakpoint is included. Evaluated at the very same 200 mm:

d = 200 mm:    argument outside the domain of the operation
d = 200 mm:    21/20 (the last row, reached -- where the band table missed)

This is not an inconsistency and it is not an oversight. A band's high bound is shared with the next band's low bound, and a value sitting on it has to belong to exactly one of the two. A breakpoint is not a boundary between rows — it is a row: the table states a value there. So excluding the last breakpoint would make the table's own final row unreachable, and the curve would answer for every input except the one it states most directly.

A test evaluates both kinds at that same number and asserts they differ, so "tidying up" the two into agreement fails there rather than quietly in somebody's method.

A value exactly on a row returns that row and is not interpolated. That is observable in exactly one place — the last row, which begins no segment at all — because interpolating across a segment a row begins would return that row's own value anyway, at weight zero.

There is no extrapolation

d = 220 mm:    argument outside the domain of the operation (no extrapolation past the last row)

Interpolation between two rows yields a value the table's author implied. Running the final segment's slope onwards yields one nobody implied — invented from the slope of wherever the table happened to stop. Clamping to the final row invents a different one. Both are the lie the whole "a miss is not a value" section refuses.

An empty table is valid and always misses; a one-row table is valid and answers at exactly its own key and nowhere else. Neither is malformed; both are merely narrow, and a table that leaves part of the domain undefined is something this library can say honestly.

A lookup inside another lookup

A lookup's operand is an ordinary expression, so it can be another lookup. This is a real published-method shape rather than a curiosity: a curve produces a continuous factor, and a second table buckets that factor into the class the method actually applies.

[[nodiscard]] constexpr auto classFactor()
{
    return formula::banded_lookup<unit::Percent, ClassBands, unit::Percent>(sizeCurveFactor(),
                                                                            { rat(95), rat(100), rat(105) });
}

It composes on every surface at once. It renders, nesting the inner call where the operand goes:

nested:        lookup(interpolate(d, at 100 mm gives 95 %, at 150 mm gives 100 %, at 200 mm gives 105 %), 90 to under 100 % gives 95 %, 100 to under 110 % gives 100 %, 110 to under 120 % gives 105 %)

it evaluates, the inner answer becoming the outer key:

d = 120 mm:    19/20 (curve gives 97 %, which falls in the 90-to-under-100 % band)

it documents, the symbol table reaching through both tables to the one quantity underneath:

nested symbols: 1

and it traces, each kind naming the row it answered from:

1. d = 120 mm
2. interpolate(#1) = 97 % [between 100 and 150 mm]
3. lookup(#2) = 95 % [90 to under 100 %]

How a lookup renders

A band is written <low> to under <high>, and that is the spelling of a half-open interval everywhere in this library — in the plain rendering, in Markdown, in LaTeX, and in the trace.

It is spelled that way because the obvious mathematical notation is Markdown link syntax. Phase 8 of this project rendered a rounding step as round[to 1 dp of mm](d); in CommonMark that is [text](url), and renderers silently dropped the operand and published a broken line. A test now asserts that no Markdown rendering contains ]( or a bare [, and a bracketed interval is exactly the character sequence that would defeat it. The wording chosen carries no punctuation at all, so it survives every Markdown flavour untouched:

banded (md):   lookup(`d`, 0 to under 100 mm gives 95 %, 100 to under 150 mm gives 100 %, 150 to under 200 mm gives 105 %)

(The backticks around d are Markdown's, marking the symbol as code; the rows are the same bytes the plain rendering produced.) examples/lookup_tables.cpp checks that this rendering contains no [ at all, rather than trusting it.

An interpolating table renders its rows with at, never with interval wording, for the reason the section above gives: a rendering that spelled a breakpoint as an interval would claim the table said something it does not, and at the last row it would exclude the one key the table states most directly.

The three kinds share one call shape — <name>(<what is looked up>, <row>, <row>, …) — the shape round(d, to 1 dp of mm) already has. Every field is a self-describing clause rather than a positional argument whose meaning a reader has to know, and the top-level separator is a comma, which means nothing to any Markdown flavour, where [, ], { and } all do. An empty table renders as no rows rather than as a complete-looking call with the table silently absent.

All three also render in LaTeX. The Gallery shows each kind typeset, alongside a worked derivation that uses all three in one expression.

How a lookup documents

A table is the part of a method that carries a source, so a citation on a lookup is the normal case, not the exotic one. documented() wraps a lookup exactly as it wraps anything else — there is nothing special to do:

[[nodiscard]] constexpr auto correctedStrength(LookupExampleShape shape)
{
    return formula::documented(var<MeasuredStrength> * sizeFactor() * shapeFactor(shape),
                               { .title = "Corrected compressive strength",
                                 .reference = "Example Standard 8:2020",
                                 .section = "7.3",
                                 .equation = "(5)",
                                 .text = "The measured strength is corrected for specimen size and for specimen "
                                         "shape, each factor taken from the table the method publishes for it." });
}

document() walks through all three kinds — the nested example above documented an interpolating one — collecting the citation and the symbol table. Here it is over a banded and an exact lookup inside one formula:

method:        f_m * lookup(d, 0 to under 100 mm gives 95 %, 100 to under 150 mm gives 100 %, 150 to under 200 mm gives 105 %) * lookup(key 7, key 3 gives 100 %, key 7 gives 97 %, key 11 gives 92 %)
cited:         Corrected compressive strength, Example Standard 8:2020, 7.3 (5)
symbol:        f_m = measured compressive strength [MPa]
symbol:        d = specimen diameter [mm]

Two symbols — f_m and d — for a formula holding two tables. The banded lookup contributes its operand's symbol; the exact lookup contributes none at all, because it reads no quantity: its row is chosen by a key, which is data on the node rather than a sub-expression.

How a lookup traces

A lookup gets a step of its own, and the step ends in a bracketed clause saying where the answer came from — the band a value fell in, or the two rows an interpolation drew on:

1. f_m = 40 MPa
2. d = 120 mm
3. lookup(#2) = 100 % [100 to under 150 mm]
4. #1 * #3 = 40000000
5. lookup(key 7) = 97 %
6. #4 * #5 = 38800000
7. #6 = 38800000 [Corrected compressive strength, Example Standard 8:2020, 7.3, (5)]

The exact lookup on line 5 adds no such clause, and that is right: its key is already the subject of the line, and the key is the row.

An interpolating lookup has two such clauses rather than one, and they say genuinely different things. Between two rows:

1. d = 120 mm
2. interpolate(#1) = 97 % [between 100 and 150 mm]

and on a row:

1. d = 200 mm
2. interpolate(#1) = 105 % [on the row at 200 mm]

The first tells a reader there is an interpolation to check and that the answer appears in neither named row; the second tells them the table stated that number directly and there is nothing to check. Neither spelling is an interval: between 100 and 150 mm names two rows and claims nothing about either end being included or excluded, so it needs neither a band's to under nor a curve's closed to.

Both lookup steps report in the unit their own table is stated in — 100 %, 97 % — while lines 4 and 6 report the products in coherent SI, because an intermediate that no quantity declares a unit for has none to be shown in. That is ordinary trace behaviour rather than anything to do with tables; see Tracing and audit trails.

On a miss, that clause is what keeps the line from lying:

1. d = 200 mm
2. lookup(#1) = argument outside the domain of the operation [in no band; the bands cover 0 to under 200 mm]

All three kinds report every failure through one error channel, so a lookup step carrying DomainError is ambiguous on its face between "the value fell in no band" and "something below me failed and I am relaying it". Without the clause, the line would read as a domain error for a case where nothing was outside any domain and the real failure happened two levels down. So the clause says which: a miss names what the table actually covers, a relayed failure says carried up from and names the operand, and an interpolation that overflowed on its own arithmetic says so rather than blaming anything beneath it.

Tracing a miss

formula::explain() goes through the throwing evaluate(). A miss is an ordinary outcome for a lookup rather than a defect, so explaining a formula that misses throws formula::ArithmeticException — carrying argument outside the domain of the operation — instead of handing back the derivation that says why it missed. (Measured, with the other direction as a control: the same explain() call over a value the table does cover returns normally.) Build the sink yourself:

template <typename Result, typename N, typename Env>
[[nodiscard]] std::string tracedEvaluation(N const& node, Env const& environment)
{
    formula::Trace<> trace {};
    formula::RecordingSink<> sink { trace };
    [[maybe_unused]] auto const outcome = formula::checked_evaluate<Result>(node, environment, sink);
    return formula::render_trace(trace, { .maxSteps = 10 });
}

checked_evaluate reports the miss in its return value and leaves you the trace, which is the whole point of having one.

One representation: Rational

All three kinds refuse to compile for any representation but Rational, and the reason differs per kind rather than being copied across.

Deciding which band a value falls in is exactly the operation binary floating point is unreliable at: a value a few ULPs off an intended boundary picks the wrong band, silently, with no error to report. An interpolating lookup has that same ground and a stronger one of its own — it computes, so a representation that rounds hands back a number that is not the one the rows imply.

Deciding whether two enumerators are the same is not arithmetic at all and would be exact in any representation; what closes Rep for the exact lookup is the lookup family speaking with one voice, so that a formula containing a table evaluates in Rational full stop, rather than in whichever representation happens to be legal for the kind that got used.

checked_evaluate<Result> always computes in Rational internally, so this restriction is never reached from the entry point you actually call.

Every citation here is invented

Every table, threshold and citation on this page and in examples/lookup_tables.cpp is made up — generic physics with fictional Example Standard references, never a real one, for the reason Citations and rendering gives in full: a real test method's clause numbers, thresholds and table values are copyrighted material, and this is a public repository.