The native kernels a matching step repeats once per node and once per hypothesis.
The Elixir arithmetic in each calling module is the reference: it is what the tests describe, what runs on a machine without a Rust toolchain, and what these kernels are held to by parity tests. They exist because that reference pays Elixir's price per value — a boxed float, a list cell, a tuple — where the answer is the same arithmetic over a few thousand rows.
A NIF is a library the BEAM loads into itself, so its absence is a raised
ErlangError rather than a returned error: loaded?/0 is the question a
caller asks, and every caller has a path for the answer false.
Turning them off
loaded?/0 also answers false when the application is configured with
config :ex_tbp_monty, native_kernels?: false. That is how a test asks for the
reference path on a machine that has the kernels, and how a run can be compared
with itself: two runs of the same episode, one with the switch either way, must
reach the same numbers.
Summary
Functions
The first index holding the highest value of a column — numpy's argmax.
True, from the native side — the only way to know it is loaded.
The evidence column after a step, and what its most likely hypothesis earned.
A kernel's answers as Elixir values: a NaN is nil.
Where every hypothesis' search location moves to, computed natively.
The feature evidence of every row, computed natively.
The rows of a column at the given indices, in the order asked for.
The hypotheses whose evidence has reached threshold, in index order.
The hypotheses whose evidence is within percentage per cent of the best.
Which hypotheses a slope threshold keeps, one byte each: 1 kept, 0 not.
Whether this VM built and loaded the native kernels, and the configuration has not turned them off.
The lowest evidence in the column — the threshold that updates everything.
The best evidence around each hypothesis, computed natively.
The bytes of a missing value: the IEEE quiet NaN.
A patch's principal curvatures at its middle pixel, computed natively.
The index of the second highest value — the runner-up hypothesis.
One channel's evidence, spread over the whole hypothesis set.
The indices of the count highest values in a column, lowest first.
The average per-channel slope of every hypothesis' evidence window.
One column written into every row of evidence: values at offset in each
stride-long row, everything else untouched.
Functions
The first index holding the highest value of a column — numpy's argmax.
column is n f64; the answer is one u32. The reference is
EvidenceGraphLM's argmax/1, which reports the first of several equal maxima.
True, from the native side — the only way to know it is loaded.
The evidence column after a step, and what its most likely hypothesis earned.
past · past_weight + present · present_weight for every hypothesis, and
present at the first index holding the highest past — numpy's argmax,
which reports the first of several equal maxima. The reference is
HypothesesDisplacer.compute_evidence/6's two passes in Elixir.
A kernel's answers as Elixir values: a NaN is nil.
Read as the integers the bytes are, because a comprehension over
<<value::float-64>> does not raise on a NaN — it stops, silently returning
the answers before it.
Where every hypothesis' search location moves to, computed natively.
poses is hypotheses * 9 f64, each rotation row by row; locations is the
search location each hypothesis claims (hypotheses * 3 f64); displacement
is the three f64 the sensor moved by. The answer is hypotheses * 3 f64:
location + pose · displacement, the same three dot products in the same order
as SpatialArithmetics.add/2 over Geometry.apply/2, which is the reference
the parity test holds this to.
The feature evidence of every row, computed natively.
rows is n * width little-endian f64 with NaN where a node has no value;
specs gives each column's index and kind (two u32), params its observed
value, tolerance and weight (three f64). The answer is n f64, NaN where the
reference answers nil.
The rows of a column at the given indices, in the order asked for.
column is n * width little-endian f64, indices is k u32; the answer is
k * width f64. Narrowing a hypothesis space keeps thousands of rows — the
copy is the cost, so it happens at the width it is asked for rather than a row
at a time in Elixir.
The hypotheses whose evidence has reached threshold, in index order.
evidence is hypotheses f64; the answer is k u32. The reference is the
comprehension for {evidence, index} <- Enum.with_index(column), evidence >= threshold.
The hypotheses whose evidence is within percentage per cent of the best.
Monty's np.where(evidence > max - max / 100 * x_percent_threshold), once the
best clears floor — the module's object evidence threshold. Asked once per
object per step. The answer is k u32 indices: none when the column is empty,
when the best does not clear floor, or when any value is a NaN, because
np.max propagates a NaN and every comparison against one is false.
Which hypotheses a slope threshold keeps, one byte each: 1 kept, 0 not.
The question a module asks of its slopes is nearly always this one, and asking
it here is one crossing instead of two: the slopes never become Elixir terms,
so nothing is boxed and nothing is decoded. evidence, channels, window
and cursor are as window_slopes/4 takes them, ages is one u32 per
hypothesis, and the rule is the reference's:
keep = age < min_age || (slope is not NaN && slope >= threshold)EvidenceSlopeTracker.select_hypotheses/2 is the reference this is held to,
and it walks the same bytes in Elixir when the kernels are off.
@spec loaded?() :: boolean()
Whether this VM built and loaded the native kernels, and the configuration has not turned them off.
The lowest evidence in the column — the threshold that updates everything.
evidence_threshold_config: "all" is Monty's default and what the experiments
run, and a step asked for it once per graph. The answer is one number out of a
column of thousands, so it is read out of the bytes: decoding them into a list
to take a minimum of it was the largest single Elixir cost in a ten-object
step. nil when every value is a NaN, which is Enum.min/1's answer for a
column of nils.
The best evidence around each hypothesis, computed natively.
ids is hypotheses * k u32 as the index answered them. hypotheses is what
was asked — the search locations (3 f64 each), the hypothesis rotations (9 f64
each, row by row) and the observation's own normal and first curvature
direction (6 f64) — and graph is what was asked of: every node's location
(3 f64), its normal and first curvature direction (6 f64), whether its pose was
fully defined, and what its features were worth. params is flatness,
max_match_distance, the normal weight, the curvature weight, whether the
curvature direction was defined (1.0 or 0.0), and the evidence of a
hypothesis with no neighbour in range.
The answer is one f64 per hypothesis.
@spec not_a_number() :: binary()
The bytes of a missing value: the IEEE quiet NaN.
Erlang has no NaN — the pattern is not a float the syntax can build — so this is the one way to write the value both sides agree means "no answer": a node with no feature, a comparison that produced nothing, evidence there is none of.
A patch's principal curvatures at its middle pixel, computed natively.
points is the patch as one flat binary of count * stride f64 — three
coordinates and, last of all, the on-surface mask — and params is the width of
the Gaussian that weights the fit, whether to weight it, and whether to fit an
intercept. The answer is nine f64: the two curvatures, their directions (three
values each) and whether they were defined.
The index of the second highest value — the runner-up hypothesis.
A vote weighs the best hypothesis against the next one, so a step asks this
once per object. The Elixir it replaces decoded the column, made a tuple per
hypothesis and sorted all of them to read one index off the end; the answer is
in the bytes. nil for fewer than two values, and ties go the way
Enum.sort_by(fn {value, index} -> {value, index} end) |> Enum.at(-2) sent
them.
One channel's evidence, spread over the whole hypothesis set.
totals is hypotheses f64, what each hypothesis has earned from the channels
so far; tested is k u32, the indices a kernel answered for; values is the
k f64 it answered. The answer is hypotheses f64: total + value for a
tested hypothesis, and total + max(min(values), 0) for the rest — an
untested hypothesis must not be rewarded for a step it did not take part in.
HypothesesDisplacer.evidence_to_add/7 is the reference this is held to.
The indices of the count highest values in a column, lowest first.
evidence is nodes f64 — one feature-evidence answer per node, NaN where a
node has no features — and the answer is count u32 in the order the sampler
takes them: evidence ascending, ties by index ascending, which is how
Enum.sort_by(&{evidence, index}) |> Enum.take(-count) reads the same column.
DefaultFeatureEvidenceScorer.evidence/4 is where the column comes from and
BurstSamplingHypothesesUpdater.top_indices/2 is the reference.
The average per-channel slope of every hypothesis' evidence window.
evidence is hypotheses * window f64 in ring order — one row a hypothesis,
NaN for a slot that has not been filled — and cursor is the slot the next
update overwrites, which is also the oldest value every window holds.
channels is the channel count at the later slot of each consecutive pair.
The answer is one f64 per hypothesis: the mean of the steps that have two
observations, NaN where the window has none. EvidenceSlopeTracker is the
reference this is held to, and it reads the same bytes when the kernels are
off.
One column written into every row of evidence: values at offset in each
stride-long row, everything else untouched.
A tracker's windows are a ring and a step writes the same slot in all of them,
so this is a copy with a column replaced. EvidenceSlopeTracker.update/3 is
the reference — it walks the same bytes in Elixir when the kernels are off.