ExTbpMonty.Core.MatcherNif (ExTbpMonty.Core v0.1.0)

Copy Markdown View Source

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

argmax_index(column)

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.

available()

True, from the native side — the only way to know it is loaded.

blend_evidence(past, present, past_weight, present_weight)

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.

decode(binary)

@spec decode(binary()) :: [float() | nil]

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.

displaced_locations(poses, locations, displacement)

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.

feature_evidence(rows, width, specs, params, increment)

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.

gather_rows(column, width, indices)

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.

indices_at_least(evidence, threshold)

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.

indices_within(evidence, percentage, floor)

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.

keep_mask(evidence, channels, window, cursor, ages, min_age, threshold)

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.

loaded?()

@spec loaded?() :: boolean()

Whether this VM built and loaded the native kernels, and the configuration has not turned them off.

min_value(column)

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.

neighbour_evidence(ids, k, hypotheses, graph, params)

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.

not_a_number()

@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.

principal_curvatures(points, stride, width, center_id, normal, params)

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.

second_argmax_index(column)

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.

spread_add(totals, tested, values)

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.

top_indices(evidence, count)

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.

window_slopes(evidence, channels, window, cursor)

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.

write_slot(evidence, values, offset, stride)

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.