Kernels: reference numbers, verification runs and design history¶
Dated record of what was measured and decided while building the three store modes
(descriptor-backed, kernel-only, item-backed), the offline tools and the verification
harnesses. CLAUDE.md keeps only what changes how to work here and points at the
section below; the header comments carry the contracts. Numbers refer to the ETH table_3
sequence of VSLAM-LAB-Benchmark unless stated otherwise. Append, do not rewrite: a later
measurement gets its own dated paragraph.
MegaLoc descriptors (2026-09-02 … 09-05)¶
Expected numbers on real sequence frames: self ≈1.0, consecutive frames ≈0.9+, unrelated places ≈0.35–0.4 (MegaLoc's common-mode floor — the reason the kernel is centred); TensorRT-vs-PyTorch cosine ≈0.99+ (worst seen 0.994, fp16 backbone / fp32 aggregator, ~4 ms/image).
TensorRT 10.3 gotcha encoded in tools/export_megaloc.py: the upstream attention
qkv.reshape(...).permute(...)[0..2] pattern is miscompiled by TensorRT's Myelin fuser
(descriptor cosine 0.05 vs ONNX Runtime, while every isolated sub-graph looks fine); the
export slices q/k/v from the projection output instead. Uniform fp16 was a separate loss
(cos 0.93), fixed by pinning the aggregator to fp32.
Standalone build outside pixi (as done for the first verification): system g++ 11 +
/usr/include/eigen3 work for the core; the viz module linked against a conda OpenCV needs
the env's libstdc++ (-DCMAKE_EXE_LINKER_FLAGS="-L<env>/lib -Wl,-rpath,<env>/lib"),
otherwise __cxa_call_terminate@CXXABI_1.3.15 is undefined at link time.
Kernel-only stores from VPR-LAB D.npy (2026-09-07)¶
Scope decision: issue #2's add_row / row-based query were deliberately not added
(decided 2026-09-07: loader only), so a kernel-only store cannot grow or answer insertion
queries — the AllFeature host therefore has no vpr: matrix backend; that is the follow-up.
The default-options set_kernel overload exists because GCC cannot use a nested struct's
member initialisers as a default argument of the enclosing class.
What D.npy is. VSLAM-LAB's <sequence>/vpr-lab/D.npy is squared_euclidean:
faiss.pairwise_distances (squared L2) on MegaLoc descriptors, then np.minimum.reduce over
four image rotations — so it is asymmetric (D_0.npy is the symmetric unrotated one) and
S = 1 − D/2. Verified on ETH table_3: only this transform reproduces MegaLoc's statistics
(floor 0.37, consecutive 0.91) and makes D_0 PSD. Rows/cols index the rgb.csv the matrix
was computed on (= Frame::frame_id when the host consumes the full csv; VSLAM-LAB's
rgb_idx/rgb_step subsetting is the caller's job via Eigen indexing).
Reference kernel_demo run on VSLAM-LAB-Benchmark/ETH/table_3/vpr-lab/D.npy (1180
frames, default --kind squared-euclidean): similarity min 0.156 / median 0.368 /
consecutive 0.909, max asymmetry 0.082 (the rotation-min), 3 negative eigenvalues (smallest
−0.014) → both WARNs fire, set_kernel 155 ms, whole run 0.65 s.
On this indefinite kernel the culler is broken, not just degraded (found 2026-09-07 when
tau 0.7 kept 970 frames and tau 0.3 kept 969): the alive unique-information scores 1/M_ii
run up to 39.7 (must be ≤ 1), the history "price" is no longer a valid bound, so history rows
cross tau and the greedy loop jams after ~210 culls whatever tau is. Sweep on D.npy,
survivors of 1180 at tau 0.1 / 0.3 / 0.5 / 0.7 / 0.9:
| variant | 0.1 | 0.3 | 0.5 | 0.7 | 0.9 |
|---|---|---|---|---|---|
| unclipped, centred | 1015 | 969 | 970 | 970 | 967 |
unclipped, --raw |
958 | 955 | 951 | 949 | 947 |
--clip (centred) |
879 | 212 | 84 | 39 | 17 |
--raw --clip |
529 | 91 | 31 | 11 | 5 |
The PSD D_0.npy (centred, no clip) gives 214 at 0.3 and 39 at 0.7, i.e. the same as clipped
D.npy. So for a rotation-min matrix always pass --clip (the demo prints a WARNING block
when it sees negative eigenvalues without clipping); clip_to_psd costs the eigenvectors
(set_kernel 155 → 810 ms at n = 1180). This is issue #3 made concrete — a host-supplied
kernel needs the PSD check, not just the unit-diagonal one. The centred first-cull degeneracy
of issue #5 also applies here in full (every view alive, no history), which is why a centred
run logs "1-2 culled views are above tau" afterwards; --raw avoids that part.
Count-driven culling and the rgb_placecell consumer (2026-09-10)¶
CullParameters::target_alive > 0 runs the same greedy order with tau = +inf (no v_i bound,
no history price test) and stops when that many views are alive in scope, never below
min_keyframes. synthetic_demo/kernel_demo only exercise the tau path; the count-driven
path is covered by VSLAM-LAB's rgb_placecell smoke test (configs/test_exp_placecell.yaml)
and a standalone Python check (1180 → 100 on ETH table_3: unique information at removal
rises monotonically 0 → 0.45, first/last frame kept, a second call with the same target culls
nothing).
First production consumer: VSLAM-LAB's rgb_placecell: <n> experiment parameter
(Capabilities/placecell.py in the VSLAM-LAB repo, pixi env placecell, task
placecell-select) loads <sequence>/vpr-lab/D.npy → similarity_from_distance("squared-euclidean")
→ set_kernel(S, frame_indices, clip_to_psd) → count-driven cull_keyframes and keeps the n
surviving frames in the experiment's rgb_exp.csv; on ETH table_3 the whole selection
(1180 → 100, centred, clipped) takes ~4 s. The clip matters there: the full D.npy has 3
negative eigenvalues, while the every-other-frame submatrix (590) is already PSD (min
eigenvalue 0.021).
COLMAP shared-information kernel (2026-09-07)¶
tools/colmap_information_kernel.py — numpy only (no pycolmap exists in any pixi env here;
own reader for COLMAP .bin/.txt models, verified identical): the pairwise
shared-information kernel of a COLMAP reconstruction, consumed through
load_npy/set_kernel/kernel_demo --kind similarity --ids.
Model. For images (i, j) the variables are x = (pose_i, pose_j, every point seen by either
image); Gauss-Newton information from the pinhole reprojection Jacobians (σ_pix = 1; left
perturbation [δθ, δt] of cam_from_world; non-pinhole models approximated by f, c with a
warning); isotropic prior Λ₀ (σ_t = median consecutive baseline in sequence order, σ_rot = 1
rad, σ_point = median distance to the cloud centroid; --prior-scale, --sigma-*) fixes the
7-DoF gauge. I_ij = ½[logdet(Λ₀+H_i) + logdet(Λ₀+H_j) − logdet(Λ₀) − logdet(Λ₀+H_i+H_j)] and
the kernel is the normalised MI K_ij = I_ij / sqrt(G_i G_j), G_i = I_ii (data
processing ⇒ K ∈ [0,1], unit diagonal). Every logdet is a Schur complement of the independent
3×3 point blocks onto the pose block; ImageInfo precomputes the single-image reduction
(per-point logdet P_k and Schur term T_k, reduced pose information A_full) so a pair
costs O(#shared points) — 53 images / 946 overlapping pairs in 0.1 s. --check N
re-evaluates N random pairs on the dense (12 + 3m)-dim system (slow: 200 pairs ≈ 6 min; worst
relative error seen 2e-10).
Gotcha that bit: COLMAP maps two keypoints of one image to the same 3D point
occasionally, so observations ≠ unique points; the per-image blocks are aggregated per unique
point (np.unique + np.add.at) and the dense check uses one column per unique point —
before that, single images matched the dense formula and pairs were off by 40–100 nats.
Ids: data-row index of --rgb-csv (the sequence's rgb.csv / rgb_raw.csv, matched by image
file name against path_rgb_0 = frame_id = D.npy numbering); without it the tool falls back
to rgb_exp.csv next to the model folder with a warning (the experiment list's numbering),
then to COLMAP image ids; unregistered images get no row. Pass the best sub-model
(<colmap_dir>/best_model names it — after a re-run of the ETH demo it was 1, and 0 held
a 2-image fragment; the tool warns when given a folder with sub-models). Output kernel.npy
(float32 NMI), mutual_information.npy (I_ij in nats, G_i on the diagonal), ids.csv
(internal_id, external_id, image_id, name, information_gain, observations).
Reference on ETH table_3 (53-image model): G_i median 1973 nats, I_ij median 33 / max
1690 nats, K over overlapping pairs 0–0.60 (consecutive frames 0.40), PSD (min eigenvalue
0.38, so no --clip needed). tau lives on a different scale than on the MegaLoc kernel:
each image's unique information 1/(K⁻¹)_ii on the raw kernel is 0.54–0.90 (an image's own
gain is dominated by points and pose the pair does not share, and its best partner explains a
median 0.46 of it), so survivors of 53 at tau 0.3 / 0.5 / 0.7 / 0.9 / 0.95 are
52 / 50 / 47 / 16 / 13 centred and 53 / 53 / 41 / 19 / 13 --raw — the useful range is
tau ≈ 0.7–0.95, and the centred first cull is again the issue-#5 degenerate one.
Design decisions and rejected alternatives (settled 2026-09-07 before implementation): the correlation analogue saturates at 0.99+ with 26 negative eigenvalues; prior scale ×0.1/×10 moves K by ≤ 0.08; shared-only points differ ≤ 0.004; info-weighted covisibility cosine correlates 0.985.
tools/colmap_kernel_demo.py (pixi run colmap-demo), the counterpart of
kernel_demo <D.npy> for the geometric kernel: runs colmap_information_kernel.py, then
kernel_demo --kind similarity --ids into <out>/selection_tau<T>[_raw]/ (dump + rgb.csv
of the survivors), then checks: survivors are registered images by name, alive + culled = the
registered set, the first frame is kept, counts agree with kernel_demo's report; exit 1 on any
failure (also when a step fails, e.g. a downsampled rgb.csv that cannot index the ids). ETH
table_3, 53 images: tau 0.9 centred keeps 16 (0, 145, 197, 254, 333, 405, 423, 495, 533,
641, 771, 869, 948, 1026, 1142, 1179 — spread along the sequence), tau 0.7 --raw keeps 41;
the whole chain runs in ~1 s.
tools/compare_kernels.py (pixi run compare-kernels): the appearance kernel (VPR-LAB
D.npy → 1 − D/2, symmetrised) and the COLMAP information kernel over the images the
reconstruction registered (selected from D.npy through ids.csv), both min-max normalised
to [0,1] off the diagonal (the VPR cosine floors at ~0.37, the information kernel lives in
0–0.6), as two heatmaps in sequence order, their difference (diverging), a scatter of paired
entries and Pearson/Spearman on stdout. ETH table_3, 53 images: raw Pearson 0.72 / Spearman
0.46, centred 0.81 / 0.58; the two agree on the diagonal band and the loop closure (first ↔
last frames), while MegaLoc gives 0.3–0.6 to pairs COLMAP finds share nothing — appearance
similarity without co-observed structure (mean VPR − COLMAP = +0.24 raw, +0.14 centred).
Figures for that run are next to the model in colmap_00000/1/information_kernel/.
Item-backed stores (covisibility kernel, 2026-09-24)¶
Designed for the covisibility kernel of AllFeature-VSLAM (docs/plans/covisibility_kernel.md
in the parent). Implementation: the first set_items call for an id appends a row (the
internal id), later calls for an alive id replace its set and the diff (std::set_difference
both ways) flows through an inverted index item_index_: item → views into an n × n integer
matrix of shared counts (item_counts_) at O(Σ posting-list lengths); item_norms_[i] = |P_i|.
The float kernel and the centring row/total sums are rebuilt from the counts lazily
(materialise_kernel_locked, O(n²), kernel_dirty_) by every kernel reader — kernel(),
snapshot()/centred_kernel(), cull_keyframes, dump, the item query — so a burst of
set_items calls costs one rebuild. The item query's dots with every stored view are its
shared counts read off the inverted index, k_i = hits_i / sqrt(|Q| |P_i|), k_xx = 1. Both
queries share explainers_locked → query_system_locked (raw or centred K_AA, k_xA) →
solve_information (jitter + LDLT). An empty set's row sum is NaN and query_system_locked
averages the centring over the m ≤ n finite rows. Profiler rows set_items (sizes: stored
views, items) and unexplained_information_items; on_set_items narrates at DEBUG; dump's
views.csv gained an items column (|P_i|, 0 in the other modes).
usable_rows change (same day): before, any NaN in a row disqualified it, so one bad
view disqualified every view (its column touches all rows) and cull_keyframes silently did
nothing; now only the culprit row (all off-diagonal entries NaN) is ignored.
Verified 2026-09-24 with a scratch parity harness against a descriptor store of normalised
indicator vectors (system g++ 11 + /usr/include/eigen3, core only): raw/centred kernels
agree to 2e-7, insertion query and culler alive_unique_information to 6e-7 raw and centred,
the Schur identity holds (query v = unique information right after insertion, 1e-6), an
incremental refresh equals a fresh store bit-for-bit, the culled set stays frozen, an empty
view leaves the other 29 rows equal to a 29-view descriptor store, and 5 injected exact
duplicates are culled at tau 0.3. synthetic_demo and kernel_demo (YOUTUBE acinipo
D.npy, --clip --tau 0.7) still pass. The bindings pass the same parity/refusal/frozen-set
checks from Python (pixi run build in the default env, Python 3.14). No SLAM run yet: the
host side is the parent's docs/plans/covisibility_kernel.md, steps 3+.
COLMAP kernel: sign fix and a ground-truth model (2026-09-30)¶
pairwise_mutual_information, the batched version that replaced the per-pair loop on 2026-09-10
(7d39dc4), added the prior term of every shared point with the wrong sign (−3 log λ_p instead
of +3 log λ_p), so every I_ij was off by 3 |shared| log λ_p nats, zero only when σ_point = 1. Found while building a ground-truth REPLICA office0 model (σ_point 1.78 m),
where the batched value disagreed with the single-pair reduction by up to 93 % and the dense
formula sided with the single pair; after the fix batched vs single-pair agree to 8.6e-13 on 200
pairs and batched vs dense to 4e-8. On ETH table_3 (σ_point 1.36 m) the fix moves K by up to
0.12 (mean 0.013), the consecutive-frame median 0.39 → 0.50, max 0.475 → 0.596, and removes the
16 negative entries. The tau-scale numbers above (unique information 0.54–0.90, tau 0.9 keeping
16 / 19 of 53) date from 2026-09-07, with the original per-pair code, and are unaffected; kernels
built between 2026-09-10 and this fix are biased and should be rebuilt.
New tools/colmap_gt_model.py builds a COLMAP text model from a database's verified matches and
a synthetic sequence's ground-truth poses and depths. On REPLICA office0: world-from-camera
poses and z-depth confirmed (0.33 px median transfer error; camera-from-world gives 231 px, ray
depth 6.7 px), pixel offset 0 fits better than COLMAP's 0.5 (0.33 vs 0.45 px), 89.5 % of 7.7 M
verified matches confirmed, 22,060 points at 0.23 px median reprojection. Filtering the matches
before the union-find is required: without it the first build merged 416 k keypoints into 12 k
tracks and failed its own check at 203 px. The resulting kernel (400 images) is PSD (min
eigenvalue 0.22), with a consecutive-frame median of 0.62 and a maximum of 0.755.