lardon3d/docs/architecture/sparse_sfm.md

1462 lines
77 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Sparse SfM / Triangulation — Gate A
## Status and boundary
**GATE A — DECISION after contract and probe study.** This document defines the
scientific and architectural contract for the Sparse SfM layer. B2 implements
the immutable v16 persistence model and its bounded readers; it does not
implement a numerical solver, change Track Model v1, change Track Builder v1,
or add a Task Kind. `FACT`, `CANDIDATE`, `DECISION` and `FROZEN` remain explicit:
the upstream Track Model and Track Builder are FROZEN, while numerical Sparse
SfM remains deferred to later gates.
**GATE B — PASS.** The v16 persistence model, migration, corruption/lifecycle
proofs, structural comparator, representative resource validation, normal
suite and ASan/UBSan closure are complete. Numerical Sparse SfM remains
deferred to Gate C and later gates.
## Scope
Sparse SfM consumes exactly one immutable, complete Track Set. It estimates a
set of camera poses and sparse 3D landmarks from coherent 2D observations. It
does not alter the Track Set, Match Results, GVRs or Feature Store. It does not
perform dense matching, meshing, texturing, metric alignment or bundle
adjustment in Gate A.
The input is one explicit Track Set identity, never “the latest Track Set” or
an enumeration of mutable project state. A disconnected image graph is
reconstructed as independent components, each with its own similarity gauge;
no metric or spatial relation between disconnected components is invented.
## Terminology and input contract
- **Image**: an acquisition with immutable pixel dimensions and an `image_id`.
- **Calibration**: the immutable intrinsic model assigned to one image or an
explicit calibration group.
- **Pose**: the rigid transform relating world coordinates to one camera frame.
- **Track**: the frozen coherent set of 2D observations from Track Model v1.
- **Landmark**: a Sparse SfM-owned 3D estimate derived from zero or one Track;
it is never stored in Track Model v1.
- **Observation coordinate**: the Feature File keypoint `x,y`, not a descriptor
vector and not a coordinate inferred from a feature index.
The Feature Store v1/v2 Feature File is the canonical coordinate source. Its
keypoint records contain binary32 `x,y`, decoded image width/height, and use a
top-left origin with +x right and +y down. A Gate B reader must page keypoint
records by index; it must not load descriptors merely to obtain coordinates.
Input validation requires the Track Set to be complete and loadable, every
referenced Feature Set and Feature File to validate, every calibration to be
present and finite, and every observation index to remain within its Feature
Set. A corrupt upstream object is a runtime/input error, not an SfM outlier.
## Camera and calibration decision
### v1 supported calibration
**DECISION: known calibration only.** Sparse SfM v1 accepts an immutable
calibration for every input image. Unknown, partially known and shared-focal
estimation are rejected until a later model gate. EXIF focal data is advisory
input for constructing a calibration, never an implicit scientific fallback.
This is deliberate for phone imagery: autofocus, digital crops, orientation,
rescaling and device variation make “all images share one perfect K” unsafe.
The calibration owner is therefore an explicit per-image or calibration-group
input whose membership and parameters are part of the reconstruction identity.
### Pinhole model
The v1 camera model is pinhole with binary64:
```text
K = [ fx 0 cx ]
[ 0 fy cy ]
[ 0 0 1 ]
```
Skew is fixed to zero. `fx > 0`, `fy > 0`, `0 <= cx < width`, and
`0 <= cy < height`. The supported distortion candidate is OpenCV-compatible
radial `k1,k2` plus tangential `p1,p2`; all four values are either supplied as
an immutable calibrated model or the model is explicitly zero-distortion.
Higher radial coefficients, rational models and thin-prism terms are not v1.
The exact distortion model and values are scientific identity fields.
### Coordinates and pose
Pixel coordinates are continuous binary64 coordinates with origin at the
top-left pixel corner, +x right and +y down. Pixel centers therefore have the
usual half-pixel interpretation supplied by the Feature File convention. A
calibrated point is undistorted first, then normalized:
```text
xn = (u_undistorted - cx) / fx
yn = (v_undistorted - cy) / fy
ray_camera = normalize([xn, yn, 1])
```
The camera frame is right-handed with x right, y down and z forward. The world
frame is also right-handed but is otherwise a gauge choice. Pose is
**world-to-camera**:
```text
Xc = R_cw * Xw + t_cw
Cw = -transpose(R_cw) * t_cw
```
The public/persisted representation is a row-major binary64 rotation matrix
plus binary64 translation. Solver-private angle-axis or quaternion variables
are permitted later. A persisted quaternion is not required, so the `q/-q`
sign ambiguity is avoided. Rendering/FreeCAD coordinate transforms are
downstream export concerns and do not change this scientific convention.
### Gauge and scale
Monocular reconstruction has a seven-degree-of-freedom similarity ambiguity.
For each connected reconstruction component, the deterministic seed camera is
the lowest canonical image ID in the selected seed pair. Its pose is fixed to
`R=I,t=0`. The second seed camera's translation direction is selected by the
deterministic essential decomposition and its norm is fixed to one arbitrary
world unit. The remaining gauge is thereby fixed to a unit seed baseline.
This unit is not metres, millimetres or any physical scale. Metric scale,
absolute orientation and georeferencing require a future explicit alignment
stage using control distances, markers or surveyed points. No fake millimetres
are inferred from focal pixels, image resolution or baseline normalization.
## Reconstruction strategy
**DECISION: incremental SfM followed by final per-component refinement.** It
matches the expected sequential vehicle/phone capture,
allows unregistered images to remain visible as a scientific result, and keeps
the active problem bounded. Global-only rotation/translation averaging would
add a larger initialization and robustness surface without a current project
requirement. A hybrid strategy is rejected for v1 complexity.
### Seed selection and relative pose
The seed is selected from the Track Set/covisibility graph, not raw Match
Results. Candidate pairs require at least the later configured minimum of
valid shared Tracks, non-degenerate essential geometry, positive-depth support
and measurable parallax. Candidates are sorted by a deterministic tuple:
```text
(-shared_track_count, -robust_parallax_score, image_id_a, image_id_b)
```
The numerical thresholds are Gate B parameter candidates and must be
fingerprinted when frozen; this tuple is the ordering policy, not a descriptor
score. Pure rotation, near-zero baseline, planar ambiguity and insufficient
cheirality reject a seed rather than inventing a scale.
Known intrinsics convert the upstream Fundamental relation into normalized
coordinates and an Essential candidate. Relative pose uses deterministic
essential decomposition with all four hypotheses tested by cheirality and
triangulation support. An F matrix is never treated as an E matrix.
### Registration
After the seed, an unregistered image is eligible when it has enough
Track-to-landmark correspondences to registered cameras. It is selected by
descending visible landmark count, then spatial-distribution score, then image
ID. Pose estimation uses a deterministic robust PnP candidate with fixed
binary64 validation, explicit iteration/confidence parameters and a local seed;
global OpenCV RNG state is forbidden. An image that cannot register remains
`UNREGISTERED` in the future SfM result and does not make the whole result a
runtime failure. Retry is bounded to deterministic graph-growth rounds; no
infinite retry loop exists.
### Components
Every connected image component is processed independently. A component with
fewer than two registered cameras has no valid 3D reconstruction. Components
with two valid cameras are allowed. Each accepted component carries its own
unit-baseline gauge and component ID; combining components requires a future
metric/alignment stage.
## Triangulation decision
**DECISION: normalized-coordinate linear DLT initialization followed by
multi-view binary64 reprojection refinement when the acceptance checks pass.**
For a Track, all currently registered observations are used in a bounded linear
system; the result is dehomogenized only when finite and well-conditioned. A
small deterministic nonlinear point-only refinement may follow. The solver does
not mutate the Track and does not split it in v1.
Accepted points require finite coordinates, positive depth in the required
observing cameras, a non-degenerate condition estimate, and reprojection
residuals within the frozen later threshold. Low parallax, planar/collinear
ill-conditioning, behind-camera points, non-finite values and excessive
reprojection error reject the landmark while leaving the source Track intact.
No arbitrary “best descriptor” or Match score is used. Pair quality is based
only on geometry; multi-view Tracks use all valid observations rather than a
random pair. Robust observation dropping is deferred: v1 rejects the landmark
as a whole, so Track identity and observation ownership remain simple.
## Gate E v1 — Final Bundle Adjustment decision
**GATE E — PASS / FROZEN.** The synchronous CPU-only final per-component
Bundle Adjustment implementation, E01--E35 matrix, normal suite, targeted
ASan/UBSan with LeakSanitizer, full sequential ASan/UBSan suite and at least
20 fresh-process E27 comparisons are validated. Gate F project orchestration
is now **PASS / FROZEN**; Gate G resource integration is **PASS / FROZEN**.
**DECISION: Gate E v1 is a synchronous, independent final per-component Bundle
Adjustment applied as post-processing to a copy of the immutable final Gate D
result.** It consumes two caller-owned immutable views that must remain coherent
for the complete call: that final Gate D result, and the same resolved
observation/calibration view used to construct the scientific Gate D input. It
never mutates either view, never creates constraints between disconnected
components, preserves each component's independent gauge, and produces a
distinct in-memory BA result.
The Gate D result alone is authoritative for final components, registered
cameras, initial poses, landmarks and the observations associated with each
landmark. The second view only resolves an observation already published by
Gate D. Its canonical key is `(feature_set_id, feature_index)`; resolution must
return the matching `image_id`, source keypoint `x,y` and immutable calibration,
and must also agree with the published Track and image identities. Missing,
ambiguous, duplicate or inconsistent resolution is a Gate E input error. Gate E
must not use array position, proximity or another heuristic fallback, add an
observation, restore a rejected association or camera, or rerun incremental SfM.
Source keypoint coordinates are the Feature File binary32 `x,y` in decoded-image
pixels, with top-left origin, +x right and +y down. Gate E converts them to
binary64 for computation; it does not treat them as already undistorted or
normalized. Given `Xc = R_cw * Xw + t_cw`, define `xn = Xc.x / Xc.z`,
`yn = Xc.y / Xc.z`, and `r2 = xn*xn + yn*yn`. The canonical OpenCV-compatible
forward model is:
```text
radial = 1 + k1*r2 + k2*r2*r2
xd = xn*radial + 2*p1*xn*yn + p2*(r2 + 2*xn*xn)
yd = yn*radial + p1*(r2 + 2*yn*yn) + 2*p2*xn*yn
u = fx*xd + cx
v = fy*yd + cy
residual = [u - observed_x, v - observed_y]
```
The residual is therefore binary64 in source pixels and uses the complete
canonical calibration model. A private Gate D validation helper that omits
distortion does not redefine this contract and is not a precedent for Gate E.
The second immutable view is an explicit scientific input, not persistence,
Project DB integration, a loader, resolver subsystem, cache, handle or Resource
System.
### Scientific and numerical contract
Gate E processes every reconstructed Gate D component independently. It
resolves the selected observations, copies the component poses and landmarks
into a private working set, builds and solves one BA problem, validates the
complete candidate, then either publishes that candidate in the distinct Gate
E result or preserves the original Gate D component. No component constrains or
influences another component.
Gate E v1 optimizes only camera extrinsic rotations, camera centers and
landmark positions. The known `fx`, `fy`, `cx`, `cy`, `k1`, `k2`, `p1`, `p2`,
observations, Track membership, identities and observation/landmark
associations are fixed and immutable. Future intrinsic optimization requires a
separate scientific and identity decision.
The public boundary remains solver-independent and world-to-camera. The private
C++ adapter uses a unit quaternion for `R_cw`, with an appropriate quaternion
manifold, and world camera center `Cw`:
```text
Xc = R_cw * (Xw - Cw)
t_cw = -R_cw * Cw
```
Conversion to or from public rotation matrices canonicalizes quaternion sign,
so `q` and `-q` cannot produce distinct observable representations. No Ceres
type crosses the future C17 ABI.
### Gate E gauge
Gate E derives deterministic BA anchors from the final Gate D result and does
not depend on historical seed IDs. In each component, the registered camera
with the lowest `image_id` is the pose anchor; its complete initial Gate D
rotation and camera center are fixed.
Among the other registered cameras, the scale anchor is the camera whose
binary64 Euclidean distance from the pose anchor is greatest. An exact distance
tie selects the lowest `image_id`; no hidden tolerance participates. For
`delta = C_scale - C_anchor`, the coordinate with greatest absolute value is
the scale axis, with exact ties resolved X, then Y, then Z. That one initial
Gate D coordinate of `C_scale` is fixed. Its other two center coordinates and
its rotation remain variable. The fixed pose removes the six rigid degrees of
freedom and the fixed nonzero scale coordinate removes the scale degree of
freedom without fixing a second pose.
The scale anchor is degenerate when:
```text
max(abs(delta.x), abs(delta.y), abs(delta.z)) <= 1e-9
```
Gate D fixes each valid component to a unit seed baseline, so `1e-9` world
units is a numerically negligible separation in that scientific gauge. A
component with no second valid camera or a degenerate scale anchor is not
optimized; its Gate D data is retained with a gauge/degenerate diagnostic.
### Objective and solver
Every valid observation contributes one two-dimensional source-pixel residual
block `f_i = [dx, dy]`, where `dx = predicted_x - observed_x` and
`dy = predicted_y - observed_y`. Gate E uses binary64 throughout. Define:
```text
s_i = dx*dx + dy*dy
delta = 2.0
delta2 = 4.0
rho_delta(s) = s if s <= delta2
rho_delta(s) = 2*delta*sqrt(s) - delta2 if s > delta2
robust_cost = 0.5 * sum_i(rho_delta(s_i))
```
Thus, with `delta = 2.0` source pixels, the second branch is
`4.0*sqrt(s) - 4.0`. The Huber loss applies once to the norm squared of the
complete 2D observation, never independently to `dx` and `dy`; the factor
`0.5` is contractual. Each observation must likewise be one 2D Ceres residual
block, not two scalar blocks.
Lardon3D computes initial and final robust costs independently of the solver
using exactly this formula, and those values govern acceptance. Ceres summary
costs may only diagnose or cross-check them. With the identical problem, a
disagreement beyond the applicable numerical tolerance stops implementation
for contract review; neither value silently replaces the other. Non-finite
`dx`, `dy`, `s_i`, `rho_delta(s_i)`, accumulation, pose, landmark or projection,
and camera-frame depth invalid under the frozen camera invariants, reject the
candidate. No clamp or fallback is permitted.
The Huber kind and scale are Gate E scientific policy, not Governor parameters,
Resource parameters or a Gate E fingerprint.
Gate E v1 selects the Ceres Solver 2.2.x API, CPU-only, with these explicit
options:
```text
minimizer_type = TRUST_REGION
trust_region_strategy_type = LEVENBERG_MARQUARDT
linear_solver_type = ITERATIVE_SCHUR
preconditioner_type = SCHUR_JACOBI
num_threads = 1
max_num_iterations = 50
function_tolerance = 1e-6
gradient_tolerance = 1e-10
parameter_tolerance = 1e-8
```
Landmark parameter blocks form elimination group 0 in increasing canonical
Track/landmark identity; camera blocks form group 1 in increasing `image_id`.
Components, cameras, landmarks, observations and residual blocks are all built
in canonical identity order. Automatic Ceres ordering is not used when the API
accepts an explicit ordering.
There is exactly one solver attempt per eligible component, with no automatic
retry, wall-clock timeout or `max_solver_time`. Environment variables, hardware
profiles, Tasks, schedulers and the Governor cannot change the single-thread
reference. `ITERATIVE_SCHUR` with `SCHUR_JACOBI` provides the required
block-sparse path without a functional SuiteSparse dependency; `SPARSE_SCHUR`,
CUDA and GPU execution are not Gate E v1.
### Eligibility, bounds and acceptance
An eligible component has at least two registered cameras, at least one valid
BA landmark, exactly resolved observations and calibrations, finite inputs, a
valid gauge, overflow-safe dimensions and no manifest underconstraint after
the anchors. Gate D already guarantees multi-view support for every published
landmark, so Gate E introduces no separate support threshold.
For a component with a valid non-degenerate Gate E gauge, let `C` be its
registered camera count, `P` its optimized landmark count and `O` its retained,
resolved observation count. Camera intrinsics and distortion are fixed. The
free tangent dimension is therefore:
```text
free_dof = 6*C + 3*P - 7
scalar_residual_count = 2*O
```
The completely fixed pose anchor removes six degrees of freedom, and the fixed
scale-anchor center coordinate removes one. Gate E v1 defines **manifest
underconstraint** as at least one of these exact structural conditions:
- **UC1:** `2*O < 6*C + 3*P - 7`, using overflow-checked integer arithmetic;
- **UC2:** an optimized landmark is observed by fewer than two distinct
registered cameras;
- **UC3:** an optimizable camera, including the scale anchor but excluding the
completely fixed pose anchor, observes fewer than three distinct landmarks;
- **UC4:** the bipartite camera-landmark optimization graph is not one connected
component containing the pose anchor.
E19 evaluates UC1--UC4 only after the existing structural validation and valid
anchor selection. E18 remains the existing insufficient-camera case and is not
redefined by E19. These conditions are necessary structural checks, not proof
of full numerical rank. Gate E v1 performs no numerical rank estimate, SVD,
singular-value or condition-number threshold, Jacobian/Hessian rank epsilon, or
Ceres covariance/rank heuristic for E19. Geometry that passes UC1--UC4 can
still be rejected by the existing projection, solver termination, finite-value,
cost non-regression and atomic-publication contracts.
UC1--UC4 are deterministic and solver-independent. Their implementation uses
the existing canonical flat Gate E working set and temporary storage bounded by
`O(C + P + O)` or better. It uses no hash-order dependency, dense `C * P`
storage, materialized rank matrix or new Resource subsystem.
Gate E retains the identically-scoped Gate D bounds of at most 4096 registered
cameras, 250,000 Tracks/landmarks and 1,000,000 observations. The Gate D
landmarks-per-growth-round bound is not a Gate E bound. All allocation and
dimension arithmetic is overflow-checked. The architecture is block-sparse;
no dense camera-count × landmark-count allocation or Jacobian is permitted.
Ceres `NO_CONVERGENCE` is rejection even if an intermediate candidate has
lower cost. Only a termination classified as successful convergence by the
private Ceres adapter is acceptable. The robust-cost comparison uses exactly:
```text
cost_tolerance = 1e-12 * max(1.0, abs(initial_robust_cost))
final_robust_cost <= initial_robust_cost + cost_tolerance
```
This comparison tolerance absorbs insignificant binary64 noise and is not a
Ceres convergence tolerance. A component is published only when its inputs are
coherent and eligible, termination is accepted, all candidate poses,
landmarks, required projections and robust costs are finite, both gauge anchors
are strictly preserved in their contract representations, the cost condition
holds, and no consumed frozen invariant is violated. Otherwise the original
Gate D component is preserved exactly and accompanied by a rejection
diagnostic. All optimization occurs on a private copy, so publication is atomic
per component and requires no in-place rollback.
### Result and diagnostics
The solver-independent Gate E result has these conceptual states:
- `COMPLETE`: at least one component is eligible and every eligible component
is optimized and accepted;
- `PARTIAL`: at least one component is accepted and at least one other eligible
component is rejected or fails;
- `FAILED`: no eligible component produces an accepted BA result, including an
input with no eligible component.
`Lardon3DSparseBundleAdjustmentStatus` contains only these three scientific
result states. In particular, `FAILED` is not an invalid-argument,
out-of-memory or internal execution error.
The synchronous execution function returns the separate,
solver-independent `Lardon3DSparseBundleAdjustmentExecutionStatus`:
```text
LARDON3D_SPARSE_BUNDLE_ADJUSTMENT_EXECUTION_OK
LARDON3D_SPARSE_BUNDLE_ADJUSTMENT_EXECUTION_INVALID_ARGUMENT
LARDON3D_SPARSE_BUNDLE_ADJUSTMENT_EXECUTION_OUT_OF_MEMORY
LARDON3D_SPARSE_BUNDLE_ADJUSTMENT_EXECUTION_INTERNAL_ERROR
```
`EXECUTION_OK` means the public input was structurally valid, Gate E reached a
complete scientific decision and produced the owned result. Its scientific
status may be `COMPLETE`, `PARTIAL` or `FAILED`; `EXECUTION_OK` with scientific
`FAILED` is valid and means that no eligible component was accepted.
`EXECUTION_INVALID_ARGUMENT` covers a violated public input contract, including
pointer/count, bounds, identity, finiteness, observation-resolution or
Gate-D/result-view coherence failures. `EXECUTION_OUT_OF_MEMORY` covers an
allocation failure, including `std::bad_alloc` caught at the C/C++ boundary,
that prevents production of a complete scientific result.
`EXECUTION_INTERNAL_ERROR` is reserved for an unexpected internal failure that
prevents safe completion; it is not a component-rejection fallback. Normal
component rejection for insufficient cameras, gauge degeneracy, manifest
underconstraint, invalid candidate projection, solver `NO_CONVERGENCE` or
`FAILURE`, a non-finite candidate or robust-cost regression contributes only to
the scientific `COMPLETE`/`PARTIAL`/`FAILED` result.
On every execution status other than `EXECUTION_OK`, the public result remains
in its canonical zero state: all counts are zero, all owned array and diagnostic
pointers are null, and destruction is safe. The execution function never
publishes a partial owned result and then returns an execution error.
Ineligible and rejected components retain their Gate D data. Each component
diagnostic contains at least component key, camera/landmark/observation counts,
pose-anchor and scale-anchor `image_id`, scale axis X/Y/Z, initial and final
robust cost, initial and final reprojection RMSE, iteration count, solver
termination class, accepted/rejected state and rejection reason. It exposes no
Ceres pointer or type.
Diagnostic reprojection RMSE is non-robust:
```text
sqrt(sum(dx*dx + dy*dy) / observation_count)
```
Acceptance remains based on robust cost and all contract invariants. Raw RMSE
is not required to improve universally in the presence of outliers.
### Reproducibility
For identical input, executable, build, dependency versions and machine with
one solver thread, component order, anchors, parameter/residual ordering,
states, accept/reject decisions and structural diagnostics are deterministic.
Comparable binary64 geometric scalars satisfy:
```text
abs(a - b) <= 1e-12 * max(1.0, abs(a), abs(b))
```
Rotations are compared geometrically rather than by raw quaternion sign. If
fresh-process tests in an identical environment cannot meet this tolerance,
implementation stops for contract review; tests must not widen it silently.
### Canonical Gate E validation matrix
| Case | Contract evidence |
|---|---|
| E01 Null/invalid input | Safe rejection; no exception crosses C |
| E02 Empty/non-eligible result | Deterministic `FAILED` with diagnostics |
| E03 Clean synthetic component | Finite accepted result, gauge held, cost non-regression |
| E04 Perturbed poses | Fixture-defined measurable improvement |
| E05 Perturbed landmarks | Fixture-defined measurable improvement |
| E06 Perturbed poses and landmarks | Convergence and fixture-defined improvement |
| E07 Noise 0.5 px | Finite accepted result or contractually justified rejection |
| E08 Noise 1.0 px | Finite accepted result or contractually justified rejection |
| E09 Noise 2.0 px | Finite accepted result or contractually justified rejection |
| E10 Outliers 10% | Huber active; finite result or clean rejection; no invariant violation |
| E11 Outliers 20% | Huber active; finite result or clean rejection; no invariant violation |
| E12 Outliers 40% | Huber active; finite result or clean rejection; no invariant violation |
| E13 Disconnected components | Independent optimization and gauges |
| E14 One success, one failure | Global `PARTIAL` |
| E15 Pose anchor | Initial rotation and center strictly preserved |
| E16 Scale anchor | Selected center coordinate strictly preserved |
| E17 Deterministic anchors | Exact distance/ID and X/Y/Z ties; `1e-9` degeneracy boundary |
| E18 Insufficient cameras | No solve; Gate D data retained |
| E19 Underconstrained geometry | No solve; Gate D data retained |
| E20 Non-finite input | Input rejection |
| E21 Non-finite projection candidate | Atomic candidate rejection |
| E22 Forced non-convergence | Private summary interpreter rejects `NO_CONVERGENCE` |
| E23 Candidate regression | Cost condition prevents publication |
| E24 Atomic rejection | Original component preserved exactly |
| E25 Canonical ordering | Explicit groups and parameter/residual order |
| E26 Same-process repeats | Structural equality and numeric tolerance |
| E27 Fresh-process repeats | At least 20 processes in one identical environment |
| E28 Ownership/destruction | Caller inputs retained; owned result safely destroyed |
| E29 Null/repeated destroy | Required only if E1 adopts the existing null-safe convention |
| E30 Allocation/overflow | Checked rejection before allocation |
| E31 Maximum boundary guards | Exact documented limits without a giant solve where isolatable |
| E32 Sparse architecture | No dense camera-count × landmark-count allocation |
| E33 Calibration immutability | Before/after identical |
| E34 Track/observation immutability | Before/after identical |
| E35 Gate D immutability | Input unchanged after success and every failure path |
E22 tests the private solver-summary-to-decision interpreter directly. It does
not expose an iteration override, add a production behavior for testing or
change `max_num_iterations = 50`. Synthetic ground-truth fixtures measure
pre/post geometric error and robust cost. Fixtures intended to improve define
their own scientifically measurable improvement; no universal pose or landmark
threshold is invented.
The complete E01--E35 matrix is implemented and validated. E27 passed at least
20 fresh processes using exact structural comparison, the frozen binary64
tolerance and geometric rotation comparison.
Local BA after registration is deferred. Gate D exposes no intermediate
scientific seam or complete registration history, and an interleaved BA could
change its subsequent growth. Introducing that policy requires a future
explicit scientific seam/version architecture decision; Gate E v1 does not
create or name such a version.
Gate E v1 remains independent of Project DB, Task Runtime, the Resource
Governor and any Resource System. It neither computes nor carries a parameter
fingerprint, defines no persistent identity, and publishes nothing. Gate F
retains project/task orchestration and persistence; Gate G retains Resource
Governor integration and final resource validation.
Ceres availability on a host remains distinct from Lardon3D dependency
declaration. Gate E declares Ceres Solver `>=2.2.0,<2.3.0` through Meson CMake
discovery and uses its 2.2.x CPU API. CUDA is not required and Lardon3D has no
functional direct SuiteSparse dependency.
## Determinism and scientific identity
Canonical order is: component image IDs, seed tuple, registration candidates,
Track IDs, observation positions, and output landmarks by `(component_id,
track_id)`. No unordered container iteration, wall-clock value, queue position,
RAM state or task ID may affect science. Binary64 is the default for geometry,
residuals and persisted values; all accepted values must be finite.
The candidate reconstruction identity is:
```text
(input_track_set_identity,
calibration_scope_identity,
sfm_kind="incremental",
sfm_version,
parameter_fingerprint)
```
Runtime task IDs, worker count, Governor state, pause timing and resource
observations are excluded. Any output-changing threshold, camera model,
initialization policy, triangulation policy, PnP policy, BA policy, precision or
loss parameter belongs in the parameter fingerprint materialized by the Gate F
persistent-identity seam. Gate D and Gate E neither receive, compute, serialize
nor transport it. Project DB stores it but does not own its meaning; Task Runtime
and the Resource System are also excluded. This seam is a Gate F orchestration
responsibility, not a new subsystem or scientific solver gate.
### Sparse SfM parameter fingerprint v1
The parameter fingerprint is `SHA-256(record_v1)`, with one hash operation over
the exact fixed record below. Its output is the complete 32-byte digest. The
record begins with the eight ASCII bytes `L3DSFMFP` (`4c 33 44 53 46 4d 46 50`),
without a NUL byte, followed by `fingerprint_encoding_version=1`. Every
multi-byte scalar is little-endian. `f64` means the exact finite IEEE-754
binary64 bit pattern, with both signed zeros encoded as positive zero; NaN and
infinity are invalid. Categorical values are the explicit `u32` policy IDs
defined here, never native enum ordinals. No native struct, padding, pointer,
`size_t`, host-endian value, JSON or locale-dependent text is hashed. A boolean,
if a future encoding uses one, is `u8`, with false `0` and true `1`; v1 has no
boolean field and no padding or reserved bytes.
| Offset | Width | Field | Canonical type | Source/value |
|---:|---:|---|---|---|
| 0 | 8 | `domain` | ASCII bytes | `L3DSFMFP` |
| 8 | 4 | `fingerprint_encoding_version` | `u32` | `1` |
| 12 | 4 | `minimum_seed_tracks` | `u32` | effective Gate D parameter |
| 16 | 4 | `minimum_seed_landmarks` | `u32` | effective Gate D parameter |
| 20 | 4 | `minimum_pnp_correspondences` | `u32` | effective Gate D parameter |
| 24 | 4 | `maximum_seed_candidates` | `u32` | effective Gate D parameter |
| 28 | 4 | `maximum_registration_rounds` | `u32` | effective Gate D parameter |
| 32 | 4 | `maximum_landmarks_per_round` | `u32` | effective Gate D parameter |
| 36 | 4 | `maximum_images` | `u32` | effective Gate D parameter |
| 40 | 8 | `maximum_observations` | `u64` | effective Gate D parameter |
| 48 | 8 | `maximum_tracks` | `u64` | effective Gate D parameter |
| 56 | 8 | `reprojection_threshold_px` | `f64` | effective Gate D parameter |
| 64 | 8 | `minimum_track_parallax_rad` | `f64` | effective Gate D parameter |
| 72 | 8 | `relative_pose.robust_threshold_px` | `f64` | effective Gate D parameter |
| 80 | 8 | `relative_pose.confidence` | `f64` | effective Gate D parameter |
| 88 | 4 | `relative_pose.max_iterations` | `u32` | effective Gate D parameter |
| 92 | 4 | `relative_pose.minimum_inliers` | `u32` | effective Gate D parameter |
| 96 | 8 | `relative_pose.minimum_inlier_ratio` | `f64` | effective Gate D parameter |
| 104 | 8 | `relative_pose.minimum_parallax_rad` | `f64` | effective Gate D parameter |
| 112 | 8 | `relative_pose.minimum_cheirality_ratio` | `f64` | effective Gate D parameter |
| 120 | 8 | `relative_pose.deterministic_seed` | `u64` | effective Gate D parameter |
| 128 | 8 | `pnp.reprojection_threshold_px` | `f64` | effective Gate D parameter |
| 136 | 8 | `pnp.confidence` | `f64` | effective Gate D parameter |
| 144 | 4 | `pnp.max_iterations` | `u32` | effective Gate D parameter |
| 148 | 4 | `pnp.minimum_inliers` | `u32` | effective Gate D parameter |
| 152 | 8 | `pnp.minimum_inlier_ratio` | `f64` | effective Gate D parameter |
| 160 | 8 | `pnp.deterministic_seed` | `u64` | effective Gate D parameter |
| 168 | 4 | `refinement.max_iterations` | `u32` | effective Gate D parameter |
| 172 | 8 | `refinement.convergence_tolerance` | `f64` | effective Gate D parameter |
| 180 | 4 | `camera_model_policy` | `u32` | `PINHOLE_K1_K2_P1_P2_V1=1` |
| 184 | 4 | `calibration_policy` | `u32` | `KNOWN_FIXED_CALIBRATION_V1=1` |
| 188 | 4 | `source_pixel_policy` | `u32` | `SOURCE_PIXEL_TOP_LEFT_X_RIGHT_Y_DOWN_V1=1` |
| 192 | 4 | `pose_policy` | `u32` | `WORLD_TO_CAMERA_R_CW_CW_V1=1` |
| 196 | 4 | `seed_ranking_policy` | `u32` | `SHARED_PARALLAX_IMAGE_ID_V1=1` |
| 200 | 4 | `robust_seed_policy` | `u32` | `LOCAL_DETERMINISTIC_SEED_V1=1` |
| 204 | 4 | `next_image_policy` | `u32` | `VISIBLE_SUPPORT_THEN_IMAGE_ID_V1=1` |
| 208 | 4 | `track_order_policy` | `u32` | `CANONICAL_TRACK_OBSERVATION_V1=1` |
| 212 | 4 | `component_policy` | `u32` | `DISCONNECTED_INDEPENDENT_COMPONENTS_V1=1` |
| 216 | 4 | `triangulation_policy` | `u32` | `NORMALIZED_DLT_POINT_REFINE_V1=1` |
| 220 | 4 | `landmark_rejection_policy` | `u32` | `WHOLE_LANDMARK_ACCEPT_OR_REJECT_V1=1` |
| 224 | 4 | `cheirality_policy` | `u32` | `POSITIVE_DEPTH_THRESHOLD_V1=1` |
| 228 | 4 | `gate_d_numeric_policy` | `u32` | `BINARY64_DETERMINISTIC_V1=1` |
| 232 | 4 | `ba_mode` | `u32` | `FINAL_PER_COMPONENT_POSTPROCESS_V1=1` |
| 236 | 4 | `local_ba_policy` | `u32` | `LOCAL_BA_DISABLED_V1=1` |
| 240 | 4 | `ba_optimized_variables` | `u32` | `ROTATION_CENTER_LANDMARK_XYZ_V1=1` |
| 244 | 4 | `ba_fixed_inputs` | `u32` | `INTRINSICS_DISTORTION_OBSERVATIONS_IDENTITIES_V1=1` |
| 248 | 4 | `ba_pose_representation` | `u32` | `UNIT_QUATERNION_R_CW_PLUS_CW_V1=1` |
| 252 | 4 | `ba_gauge_policy` | `u32` | `MIN_IMAGE_FARTHEST_CENTER_ONE_AXIS_V1=1` |
| 256 | 8 | `ba_degenerate_scale_threshold` | `f64` | `1e-9` |
| 264 | 4 | `ba_residual_policy` | `u32` | `ONE_FULL_2D_BLOCK_PER_OBSERVATION_V1=1` |
| 268 | 4 | `ba_projection_policy` | `u32` | `SOURCE_PIXEL_PINHOLE_K1_K2_P1_P2_V1=1` |
| 272 | 8 | `ba_minimum_camera_depth` | `f64` | `1e-9` |
| 280 | 4 | `ba_robust_loss` | `u32` | `HUBER_FULL_2D_NORM_V1=1` |
| 284 | 8 | `ba_huber_delta_px` | `f64` | `2.0` |
| 292 | 4 | `ba_robust_cost_policy` | `u32` | `HALF_SUM_RHO_SQUARED_NORM_V1=1` |
| 296 | 4 | `ba_solver_contract` | `u32` | `CERES_2_2_CONTRACT_V1=1` |
| 300 | 4 | `ba_minimizer` | `u32` | `TRUST_REGION_V1=1` |
| 304 | 4 | `ba_trust_region_strategy` | `u32` | `LEVENBERG_MARQUARDT_V1=1` |
| 308 | 4 | `ba_linear_solver` | `u32` | `ITERATIVE_SCHUR_V1=1` |
| 312 | 4 | `ba_preconditioner` | `u32` | `SCHUR_JACOBI_V1=1` |
| 316 | 4 | `ba_num_threads` | `u32` | `1` |
| 320 | 4 | `ba_max_num_iterations` | `u32` | `50` |
| 324 | 8 | `ba_function_tolerance` | `f64` | `1e-6` |
| 332 | 8 | `ba_gradient_tolerance` | `f64` | `1e-10` |
| 340 | 8 | `ba_parameter_tolerance` | `f64` | `1e-8` |
| 348 | 4 | `ba_retry_count` | `u32` | `0` |
| 352 | 4 | `ba_convergence_acceptance` | `u32` | `CONVERGENCE_ONLY_V1=1` |
| 356 | 8 | `ba_cost_non_regression_factor` | `f64` | `1e-12` |
| 364 | 4 | `ba_underconstraint_policy` | `u32` | `MANIFEST_UC1_UC2_UC3_UC4_V1=1` |
| 368 | 4 | `ba_numeric_policy` | `u32` | `BINARY64_SINGLE_THREAD_V1=1` |
The exact v1 record length is 372 bytes. Gate D field semantics and source
translations are exhaustive:
| Field | Source type | Encoding/normalization | Scientific meaning |
|---|---|---|---|
| `minimum_seed_tracks` | `uint32_t` | `u32` | minimum shared Tracks for a seed |
| `minimum_seed_landmarks` | `uint32_t` | `u32` | minimum accepted seed landmarks |
| `minimum_pnp_correspondences` | `uint32_t` | `u32` | minimum correspondences for registration |
| `maximum_seed_candidates` | `uint32_t` | `u32` | bound on seed attempts |
| `maximum_registration_rounds` | `uint32_t` | `u32` | bound on growth rounds |
| `maximum_landmarks_per_round` | `uint32_t` | `u32` | bound on new landmarks per round |
| `maximum_images` | `uint32_t` | `u32` | accepted input image bound |
| `maximum_observations` | `uint64_t` | `u64` | accepted observation bound |
| `maximum_tracks` | `uint64_t` | `u64` | accepted Track bound |
| `reprojection_threshold_px` | `double` | finite canonical `f64` | landmark reprojection acceptance |
| `minimum_track_parallax_rad` | `double` | finite canonical `f64` | landmark parallax acceptance |
| `relative_pose.robust_threshold_px` | `double` | finite canonical `f64` | essential robust residual threshold |
| `relative_pose.confidence` | `double` | finite canonical `f64` | essential robust confidence |
| `relative_pose.max_iterations` | `uint32_t` | `u32` | essential robust iteration bound |
| `relative_pose.minimum_inliers` | `uint32_t` | `u32` | essential minimum inlier count |
| `relative_pose.minimum_inlier_ratio` | `double` | finite canonical `f64` | essential minimum inlier fraction |
| `relative_pose.minimum_parallax_rad` | `double` | finite canonical `f64` | relative-pose minimum parallax |
| `relative_pose.minimum_cheirality_ratio` | `double` | finite canonical `f64` | relative-pose positive-depth fraction |
| `relative_pose.deterministic_seed` | `uint64_t` | `u64` | local essential robust-estimator seed |
| `pnp.reprojection_threshold_px` | `double` | finite canonical `f64` | PnP robust residual threshold |
| `pnp.confidence` | `double` | finite canonical `f64` | PnP robust confidence |
| `pnp.max_iterations` | `uint32_t` | `u32` | PnP robust iteration bound |
| `pnp.minimum_inliers` | `uint32_t` | `u32` | PnP minimum inlier count |
| `pnp.minimum_inlier_ratio` | `double` | finite canonical `f64` | PnP minimum inlier fraction |
| `pnp.deterministic_seed` | `uint64_t` | `u64` | local PnP robust-estimator seed |
| `refinement.max_iterations` | `uint32_t` | `u32` | point-refinement iteration bound |
| `refinement.convergence_tolerance` | `double` | finite canonical `f64` | point-refinement stopping tolerance |
Every row is fingerprinted. Integer fields require no normalization beyond
their fixed-width little-endian translation; every floating field uses the
canonical-zero rule above.
The policy IDs above freeze the full named Gate D and Gate E v1 semantics,
including canonical order and tie breaks,
whole-landmark rejection, positive-depth cheirality, the pose anchor chosen by
smallest image ID, the farthest-center scale anchor with exact image-ID tie,
X/Y/Z axis tie order and exactly one fixed center coordinate. The Gate E
projection is `R_cw + Cw`; intrinsics and `k1/k2/p1/p2` distortion are fixed.
The robust cost policy is exactly `0.5 * sum rho(dx^2+dy^2)` with one Huber loss
on each full two-dimensional residual. `CERES_2_2_CONTRACT_V1` denotes the
accepted Lardon3D Ceres contract `>=2.2.0,<2.3.0`, not package, build, linker or
transitive SuiteSparse metadata.
For Gate D, `WORLD_TO_CAMERA_R_CW_CW_V1` includes the frozen SO(3) residual
limit `1e-6`;
`POSITIVE_DEPTH_THRESHOLD_V1` means strict camera depth greater than `1e-9`;
and `NORMALIZED_DLT_POINT_REFINE_V1` includes the frozen homogeneous-scale
epsilon `1e-12` and collinearity covariance-determinant limit `1e-10`. These
constants are not runtime members, so their stable policy IDs, rather than
duplicate floating fields or implementation enum ordinals, own their exact
v1 semantics.
All 27 effective scalar members, including nested members, of
`Lardon3DSparseIncrementalParameters` occur exactly once. Serialization uses
the validated effective values actually passed to Gate D, so an omitted default
and the same explicitly supplied value produce identical bytes. Actual Track
Set identity, Track/Feature IDs, calibration-scope identity and individual
calibration IDs or numeric values, `sfm_kind`, `sfm_version`, project/task/
transaction/reconstruction IDs, timestamps, resource state, result values and
metrics are excluded. The separate calibration scope hash binds sorted image
IDs to calibration hashes, and each calibration hash binds dimensions,
`fx/fy/cx/cy/k1/k2/p1/p2`, model/version and provenance; the parameter record
therefore records calibration semantics without duplicating calibration
instances.
Changing only a parameter value retains encoding version 1 and naturally
changes the digest. Adding a fingerprint-owned field or changing byte layout
requires a new encoding version; changing the Sparse SfM algorithmic contract
may separately require a new `sfm_version`. Neither version substitutes for the
other. Equal complete candidate tuples therefore identify the same scientific
candidate regardless of runtime metadata.
Gate F implementation must prefer an internal, solver-independent helper unless
a separate public C17 API decision is made. It must reuse the project's SHA-256
implementation, use bounded constant-size storage without cache, scheduling,
Governor interaction or reservation, and add a golden 372-byte default record,
its expected SHA-256 digest, and mutations proving every fingerprint-owned
category changes the digest. This contract authorizes no public symbol.
Exact byte identity is not promised for a future multi-threaded floating-point
solver until measured. The v1 target is deterministic ordering and numerical
reproducibility within documented tolerances; single-threaded reductions are
the initial reference.
### Gate F publication policy
**FROZEN.** Gate F keeps the Gate E execution domain separate from the Gate E
scientific result domain. `EXECUTION_OK` is the necessary and sufficient Gate E
condition for publication eligibility. Under that execution status,
`COMPLETE`, `PARTIAL` and `FAILED` are all valid complete in-memory Gate E
results and are published atomically. A successful Project DB publication or
exact-identity reuse makes the Task Runtime execution successful for all three
scientific statuses.
| Gate E execution | Scientific status | Publish | Runtime after DB success |
|---|---|---|---|
| `EXECUTION_OK` | `COMPLETE` | exact Gate E result | success |
| `EXECUTION_OK` | `PARTIAL` | exact Gate E result | success |
| `EXECUTION_OK` | `FAILED` | exact Gate E result | success |
| `EXECUTION_INVALID_ARGUMENT` | none | no publication | failure |
| `EXECUTION_OUT_OF_MEMORY` | none | no publication | failure |
| `EXECUTION_INTERNAL_ERROR` | none | no publication | failure |
Scientific `PARTIAL` describes a complete result in which accepted components
contain validated optimized values and rejected components preserve their exact
Gate D values. It never permits partial database visibility. Scientific
`FAILED` means that no eligible component accepted Bundle Adjustment; under
`EXECUTION_OK` it remains a valid complete Gate E result whose components
preserve the Gate D values selected by Gate E.
Gate F never publishes Gate D as a fallback after a Gate E execution error. It
does not rerun Gate D or Gate E, retry automatically, reinterpret scientific
status as a Task Runtime state, or require scientific `COMPLETE` for runtime
success. A Project DB publication failure rolls back and fails the runtime
execution. The scientific status is output diagnostic metadata, not part of
the candidate identity or parameter fingerprint. Gate F uses an existing
status carrier when one exists; the absence of a dedicated Project DB v16
column does not require a migration or authorize reuse of an unrelated column.
### Gate F resource-demand boundary
**FROZEN.** Gate F materializes the immutable `Lardon3DResourceEstimate` needed
to describe its task to the existing Task Runtime. The estimate is a pure
function of durable task input shape and known implementation characteristics;
it never depends on current RAM, swap, PSI, load, queue depth, task attempt or
Governor state. Gate F submits through the existing queue and does not perform
admission or reservation itself.
The existing Governor owns admission and reservation policy. Gate G owns
telemetry, pressure and scheduling policy, but Gate G core neither changes the
frozen Sparse SfM producer estimate nor adds scratch support. The estimate never
enters the 372-byte parameter record, candidate identity, `sfm_version` or scientific decisions, and cannot
change Gate D or Gate E parameters. This clarification preserves
`NO_NEW_SUBSYSTEM` and the mandatory reservation invariant.
Gate G G0a freezes consumption of the exact Gate F v1 estimate. A restored task
keeps its persisted estimate and is evaluated with newly captured machine
telemetry; it is never recomputed with later coefficients. A future formula
change requires a separate operational formula/version review for newly created
tasks and cannot affect F0, candidate identity, Gate D/E parameters or existing
tasks. Sparse SfM remains batch one and has no scratch, spill or out-of-core
path. Swap, zram and external storage do not enlarge its RAM capacity.
### Gate F durable task payload
**FROZEN.** Gate F advances the current Project Database schema head from v16
to v17 with one strictly additive `sparse_sfm_tasks` table. The historical v16
migration and its immutable reconstruction model remain unchanged. The new
table follows the existing one-to-one typed-task pattern: its primary key is a
foreign key to `tasks(task_id)` with cascade cleanup, and creation records the
generic task snapshot and typed payload in one transaction.
The payload stores the immutable Track Set reference, calibration-scope
reference, Sparse SfM kind and version, and every one of the 27 effective
scalars in `Lardon3DSparseIncrementalParameters`, using their existing fixed
integer widths and exact finite SQLite binary64 values. The Task Kind version
selects the payload interpretation. Reload never reapplies defaults and rejects
a missing row, incompatible kind/version, invalid identity or invalid
parameter. The generic checkpoint codec remains v1 and unchanged.
The parameter fingerprint is not stored in `sparse_sfm_tasks`. Reconstruction
loads and validates the effective fields, rebuilds the unchanged 372-byte F0
record and recomputes SHA-256. Calibration values, Track observations,
ResourceEstimate and runtime metadata are not duplicated in the typed payload.
This is an additive Project DB/typed-Task extension, not a generic payload or
persistence subsystem.
### Gate F closure decisions
**FROZEN.** The v1 declarative estimate uses the immutable pre-admission counts
`I` (distinct participating images), `T` (Tracks) and `O` (Track observations):
```text
raw = 134217728 + I*65536 + T*2048 + O*512
memory_fixed_bytes = raw rounded upward to a whole MiB
```
Every operation is checked `uint64_t` arithmetic; overflow rejects task
creation before persistence. The remaining estimate is RAM-per-item 0, all GPU
fields 0, minimum and maximum batch 1, one CPU thread, one IO slot and CPU task
class. These conservative coefficients cover Gate D containers, adapters,
camera/landmark state, Gate E copies, ordering/residual storage and Ceres
working storage. They are operational, machine-state independent and excluded
from scientific identity. A restored task uses its persisted generic estimate.
The four full-domain `uint64_t` payload values (`maximum_observations`,
`maximum_tracks` and both deterministic seeds) are individual exact eight-byte
little-endian SQL BLOBs. Any other storage class or length is corrupt; there is
no signed cast, text, REAL conversion or domain restriction. Their F0 encoding
remains unchanged.
Gate D `COMPLETE` and usable `PARTIAL` results proceed to Gate E. Gate D
scientific `FAILED` fails the Task, invokes no Gate E and publishes nothing.
Gate D invalid input or allocation failure likewise fails execution. A claimed
usable result that violates frozen structure is an internal integration
failure. Once Gate E is legitimately reached, every `EXECUTION_OK` scientific
status remains publication-eligible.
Gate F computes publication diagnostics from every retained observation of the
exact final Gate E result. With source-pixel residual `dx,dy`, each observation
contributes `s=dx*dx+dy*dy` and `e=sqrt(s)`. Global RMSE is
`sqrt(sum(s)/N)`. Global median is the middle sorted `e`, or for even `N`,
`lower + (upper-lower)/2`. Projection is the frozen Gate E distorted pinhole
model, binary64, with strict minimum depth. Empty, non-finite or invalid
projection input fails publication; no observation is skipped or clamped.
Metrics are deterministic result diagnostics, never candidate identity.
### Gate F durable reconstruction projection
**FROZEN.** Gate F projects the complete Gate D/E scientific result onto the
existing Project DB reconstruction model. A component belongs to the durable
projection exactly when `registered_image_count > 0` and `landmark_count > 0`.
Every such component is persisted with its exact final Gate E geometry. This
includes a BA-rejected component whose valid Gate D geometry Gate E preserved.
A non-reconstructed graph component failing either predicate remains an
ephemeral scientific/orchestration diagnostic and has no Project DB component
row. Gate F fabricates no camera, landmark or placeholder and does not fail an
otherwise publishable reconstruction merely because such diagnostics exist.
The projected result must still satisfy every top-level Project DB invariant;
otherwise publication does not occur and the Task fails.
Persisted component and geometry counts describe only this durable projection.
Global reprojection metrics likewise include exactly the retained observations
belonging to persisted geometry. Omission neither renumbers scientific
component keys nor changes candidate identity. No persistent diagnostic table,
sidecar, metadata blob or schema beyond v17 `sparse_sfm_tasks` is introduced.
Gate F v1 is **PASS / FROZEN**. Gate D and Gate E remain **PASS / FROZEN**;
Gate G architecture decisions and implementation are **PASS / FROZEN**.
### Gate F validation closure
**PASS / FROZEN.** Gate F freezes the F0 372-byte parameter record v1 and its
SHA-256 digest, Project DB v17 typed payload, `sparse_sfm.run` version 1,
pre-admission declarative estimate, governed Task Runtime execution, durable
replay, deterministic D→E orchestration, exact candidate reuse and atomic
reconstruction publication. Canonical gate progression:
```text
Gate A — PASS / FROZEN
Gate B — PASS / FROZEN
Gate C — PASS / FROZEN
Gate D — PASS / FROZEN
Gate E — PASS / FROZEN
Gate F — PASS / FROZEN
Gate G — PASS / FROZEN
```
The five candidate-identity dimensions remain separate:
```text
(
input_track_set_identity,
calibration_scope_identity,
sfm_kind,
sfm_version,
parameter_fingerprint
)
```
The F0 golden SHA-256 digest remains
`e1c83e5b2036e49254a9426ddbace42b7831373bc896f27abdd2f61e302f9e8c`.
Final validation completed with the normal suite at 41/41, targeted Gate F
ASan/UBSan/LeakSanitizer at 4/4 with leak detection enabled, and the full
sequential ASan/UBSan suite at 41/41. Fresh-process validation passed 20/20 for
the Gate F contract and 20/20 for the production `sparse_sfm.run` task. The C17
public-header probe and `git diff --check` passed. Final human diff review
passed; no Gate F implementation work, validation work or human decision
remains.
## Future persistence and API candidates
No Project DB v16 is created in Gate A. A later model gate may define immutable
entities such as `sparse_reconstructions`, registered camera poses, landmarks,
and landmark observations. The reconstruction must reference exactly one Track
Set and calibration identity, publish atomically, and never expose half-solved
cameras or points. Upstream Track Set deletion policy requires an explicit
future ownership decision; silent CASCADE of a published reconstruction is not
assumed.
The future public boundary remains C17-safe and solver-independent. Candidate
opaque APIs accept immutable Track Set/calibration inputs and return owned
opaque result pages with explicit free functions. No `cv::Mat`, Eigen, Ceres,
STL, callback or C++ exception crosses the boundary. Numeric kernels operate on
pure in-memory structures and never open SQLite or query the Governor.
## Resource envelope
Let `C` be registered cameras, `T` Tracks, `P` active landmarks, `O`
observations and `E_covis` sparse image-graph edges. The architecture requires
`O(C + T + P + O + E_covis)` memory for graph/index structures plus the solver
working set. It forbids a dense `C×P`, `C×C` or co-visibility matrix. Track
length has no arbitrary 256 cap; long Tracks are iterated through checked
bounded storage.
Triangulation/registration are light CPU units and can be batched. Gate E v1
uses local scientific limits for its final per-component BA and does not query
the Governor. Gate G consumes the frozen Gate F estimate derived from immutable
workload shape without changing scientific results. The existing Resource
Governor owns RAM/PSI/swap policy; Sparse SfM adds no system-pressure thresholds.
Swap is never normal working memory, and UMA RAM must preserve several GiB of
desktop/iGPU headroom.
## Hardware and probe study
Gate A preflight measured 16 logical CPUs, `MemTotal=15597716 KiB`,
`MemAvailable=8245288 KiB` at the study point, an 8 GiB swapfile, a 6 GiB
zram device, and zero current memory/IO PSI average. The host is the Ryzen 7
8845HS/Radeon 780M UMA target described by the performance document.
The project already links OpenCV 5.0.0. Host-installed libraries and their
pkg-config or CMake discovery metadata are capabilities, not Lardon3D
production dependencies. Gate E now declares Ceres 2.2.x through Meson CMake
discovery. No system setting, swap device or GPU mode was changed.
Gate A probes use deterministic synthetic camera arcs, controlled noise and
degenerate planar/pure-rotation cases. Every RSS probe is a separate normal
optimized child process; fixture arrays, solver structures and peak RSS are
reported separately. Thread probes are limited to 1/2/4/8 threads and stop if
MemAvailable, swap, PSI or desktop responsiveness becomes unhealthy. No
production Sparse SfM code is created by this gate.
## Gate decomposition
- **Gate B — PASS / FROZEN — Sparse Reconstruction Model:** immutable in-memory model, result
states, calibration ownership and candidate persistence contract; no DB v16
until this contract is reviewed.
- **Gate C — PASS / FROZEN — Geometry primitives:** normalized camera model, relative pose,
deterministic seed, triangulation and PnP with synthetic ground truth.
- **Gate D — PASS / FROZEN — Incremental core:** registration ordering, components,
unregistered-image policy and deterministic reconstruction output.
- **Gate E — PASS / FROZEN — Final Bundle Adjustment:** synchronous final per-component BA on a
copy of the immutable Gate D result, with its scientific and numerical
contract frozen here; interleaved local BA is deferred.
- **Gate F — PASS / FROZEN — Project orchestration:** explicit Track Set/calibration input,
atomic publication and durable runtime integration.
- **Gate G — PASS / FROZEN — Resource/freeze:** Governor
admission, sustained hardware safety and recovery are implemented and fully
validated.
## Algorithm comparison and Gate A evidence
### Incremental SfM
Seed/order risk is controlled by deterministic policy. It is robust for
sequential capture, has canonical queues and seeds, moderate complexity, and
sparse `C,T,O` scaling followed by final per-component BA. **SELECTED v1.**
### Global SfM
Global averaging can spread weak geometry. It is sensitive to disconnected or
weak-baseline graphs, needs several global tie policies, and requires a larger
sparse solve. **Rejected for v1.**
### Hybrid
Hybrid design combines both failure surfaces, is hard to specify minimally and
harder to reproduce. **Rejected for v1.**
Triangulation candidates:
- Midpoint/ray only: fragile with noise and awkward beyond two views. Rejected.
- Linear normalized DLT: good initialization with explicit checks and all
registered observations. **Selected initialization.**
- DLT plus point-only refinement: better residual with bounded per-point work
and fixed termination. **Selected v1 candidate.**
| BA candidate | Sparse support | Dependency status | Decision |
|---|---|---|---|
| Dense normal equations | Prohibited for serious `C×P` problems | No | Rejected |
| OpenCV generic optimization | Not a sparse BA contract | Present, wrong abstraction | Rejected |
| Ceres 2.2.x iterative Schur | Block-sparse | Declared through Meson CMake discovery | **Implemented Gate E v1** |
### Synthetic geometry probe
The normal OpenCV 5.0.0 installation was exercised in a fresh Python process on
100 deterministic points, binary64 K (`fx=fy=800`, `cx=640`, `cy=480`), a one-unit
baseline and a four-degree rotation. `recoverPose` retained 100 inliers with
zero measured rotation error and translation direction absolute dot product
`0.997564`; two-view DLT triangulation had median position error
`2.73e-15`; iterative PnP retained 100 inliers with camera-center error
`9.02e-8` and zero measured rotation error. This validates the candidate
primitive boundary, not production SfM correctness.
The same probe deliberately tested pure rotation and planar points. OpenCV can
still return an Essential matrix with 100 nominal inliers in both cases; this
is why `findEssentialMat` success is not an acceptance criterion. Seed
selection must apply parallax, conditioning, cheirality and model-ambiguity
checks before accepting a component.
### Dependency and hardware evidence
The project already links OpenCV 5.0.0. Host probes found Eigen 5.0.1, BLAS
3.12.0, LAPACK 3.12.0 and TBB 2023.1 as host capabilities or transitive
facilities rather than current Lardon3D production dependencies. Ceres may use
CMake discovery, so pkg-config alone does not establish host availability.
Ceres 2.2.x is the implemented Gate E scientific API and is declared through
Meson CMake discovery. The measured machine has 16 logical CPUs,
`MemTotal=15597716 KiB`, `MemAvailable=8245288 KiB` at preflight, an 8 GiB
swapfile, 6 GiB zram and zero memory/IO PSI averages at the probe start. Gate E
uses one solver thread; future Gate G resource admission cannot change that
scientific setting.
## Gate A unresolved boundaries
The following remain deliberately deferred rather than hidden: Ceres
licensing/dependency integration, metric alignment, persistent orchestration
and durable SfM checkpoints. Gate E freezes its own robust loss, convergence,
ordering and acceptance policy here without introducing persistence or a
fingerprint.
## Gate C — pure calibrated geometry
**GATE C — PASS.** Pure calibrated geometry primitives, synthetic ground truth,
degeneracy rejection, determinism, normal suite and ASan/UBSan validation are
complete. Incremental orchestration, BA and persistent geometry integration
remain later gates.
Gate C keeps geometry outside Project DB and exposes a C17-safe, synchronous
pure-primitive boundary. Inputs are binary64 calibrated pixels, fixed
world-to-camera poses, and caller-owned correspondence arrays; no primitive
opens SQLite, reads Feature Files, loads images or invokes the Task Runtime.
The v1 candidate uses OpenCV 5.0.0 `calib3d` operations with every scientific
parameter supplied by an explicit configuration structure. Public outputs use
row-major binary64 `R_cw` and `t_cw`; relative translation has unit norm and no
metric interpretation.
The candidate contract requires deterministic caller ordering, finite inputs,
explicit robust-estimator thresholds/confidence/iteration limits and a local
seed. Essential hypotheses are accepted only after explicit positive-depth
support, rotation validation, parallax and reprojection checks. Pure rotation,
low parallax, weak conditioning and non-finite results are failures. Two-view
and multi-view points use normalized-coordinate linear DLT followed by bounded
point-only binary64 refinement; PnP returns world-to-camera pose with explicit
cheirality and inlier diagnostics. The tested v1 parameter set is frozen by the
Gate C ground-truth and degeneracy evidence; future orchestration may choose
other explicitly fingerprinted configurations.
### Gate C tested threshold set
The pure API has no hidden defaults; callers provide all acceptance settings.
The Gate C reference matrix uses the following reproducible set:
| Parameter | Value | Unit/purpose |
|---|---:|---|
| Relative robust threshold | 1.0 px clean; 1.5 px matrix | pixel residual |
| Relative confidence | 0.999 | RANSAC confidence |
| Relative iterations | 1000 clean; 1500 matrix | iterations |
| Relative minimum inliers | 6 clean; 24 matrix | correspondences |
| Relative minimum ratio | 0.75 clean; 0.5 matrix | fraction |
| Minimum parallax | `1e-4` rad | seed geometry |
| Minimum cheirality ratio | 0.5 | positive depth |
| PnP threshold | 1.0 px clean; 1.5 px matrix | pixel residual |
| PnP confidence | 0.999 | RANSAC confidence |
| PnP iterations | 1000 | iterations |
| PnP minimum inliers | 6 clean; 12 matrix | correspondences |
| PnP minimum ratio | 0.75 clean; 0.5 matrix | fraction |
| Point refinement tolerance | `1e-12` | normalized residual |
| Point refinement iterations | 30 | iterations |
Degeneracy checks use finite values, positive depth, rotation SO(3) residual
`1e-6`, depth epsilon `1e-9`, homogeneous scale epsilon `1e-12`, and
collinearity covariance determinant `1e-10`. These are pure-geometry
parameters and do not alter Project DB identity.
## Gate D — incremental Sparse SfM core
**GATE D — PASS / FROZEN.** Gate D is the first executable link
between the immutable Track/Calibration contracts and the Gate C primitives.
The reference implementation is synchronous, deterministic, CPU-only,
in-memory, bounded and independent of Project DB, Task Runtime, Resource
Governor and persistence publication.
### Inputs
Gate D consumes exactly one immutable Track Set, one immutable calibration
scope, finite calibration values for participating images, bounded keypoint
coordinates addressed by `(feature_set_id, feature_index)`, and explicit
parameters immutable during execution. Gate D neither computes nor carries the
parameter fingerprint materialized later at the Gate F persistent-identity
seam. The Track Set is never mutated.
### Algorithm
The core sorts image and Track identities, builds sparse connected components,
orders seed candidates by shared Track count and image IDs, and tries a bounded
number of seeds. Each candidate uses the Gate C relative-pose, cheirality,
parallax and two-view triangulation contracts. A valid seed establishes a
component-local unit gauge.
Unregistered images are then ordered by visible accepted-landmark count and
image ID. Gate C calibrated PnP registers at most one selected image per
bounded round. Failed registration leaves the image explicitly unregistered.
New landmarks use all currently registered observations, Gate C multi-view DLT
and bounded point-only refinement. A landmark is accepted or rejected as a
whole; Track observations are never dropped or rewritten.
After each successful camera registration, an existing landmark whose Track
has gained registered observations is reconsidered in canonical image-ID
order. Gate C multi-view triangulation and point refinement use the complete
eligible observation set. The replacement is published in memory only after
finite-value, positive-depth and reprojection validation; otherwise the prior
valid landmark and its observations remain unchanged.
The Gate D reference bounds are 4096 input images, 250,000 Tracks, 1,000,000
observations, 32 seed candidates, 32 registration rounds and 4096 new
landmarks per growth round. The defaults use 1.5 px relative-pose/PnP robust
thresholds, a 2.0 px landmark reprojection threshold, 0.5 minimum inlier
ratios, `1e-4` rad minimum parallax, 6 minimum seed/PnP inliers and 30
point-refinement iterations. These are Gate D policy defaults; changing them
changes the explicit parameter configuration.
### Output and failure semantics
The in-memory result contains deterministic components, registered cameras,
accepted landmarks, landmark observations, reprojection diagnostics and
explicit unregistered images. Results are `COMPLETE`, `PARTIAL` or `FAILED`.
Invalid input fails before computation. A rejected seed, camera or landmark
does not corrupt an accepted model. No partial result is persisted.
Growth stops immediately when a complete registration round cannot register
an image. It also stops exactly at the configured registration-round bound.
Both paths retain valid cameras and landmarks, list every remaining image as
unregistered and produce `PARTIAL` when usable geometry exists.
Components with fewer than two registered cameras are not valid 3D components.
Disconnected valid components retain independent unit gauges and are never
globally aligned by Gate D.
### Gate D limits
Gate D does not implement BA, persistence adapters, Task Runtime, checkpoints,
Governor integration, a Resource System, GPU execution, dense reconstruction,
metric alignment, viewer integration or any Project DB change. BA remains the
separate PASS / FROZEN Gate E post-processing stage; project/task orchestration
remains Gate F and resource/freeze integration remains Gate G.
### Canonical Gate D functional matrix
This table freezes the complete numbered validation contract. Evidence is the
minimum dedicated observation required; an earlier rejection never substitutes
for the named path.
| Case | Purpose | Required path and evidence | Expected result |
|---|---|---|---|
| 01 Minimal two-view | Smallest valid reconstruction | One seed, two cameras, finite landmarks | `COMPLETE` |
| 02 Deterministic seed | Canonical seed identity | Same selected pair and pose on repeat | `COMPLETE` |
| 03 Multiple seed candidates | Candidate ordering | Multiple eligible pairs, canonical first pair | `COMPLETE` |
| 04 Rejected first seed / later seed | Seed fallback | At least two attempts, later pair selected | `COMPLETE` |
| 05 Camera-addition order | Registration ordering | Highest support then image ID, one per round | `COMPLETE` |
| 06 Clean PnP | Nominal registration | PnP attempted and succeeds with clean support | `COMPLETE` |
| 07 Noisy PnP | Bounded noise | PnP succeeds with finite pose | `COMPLETE` |
| 08 Deterministic PnP outliers | Robust registration | Stable inlier count and pose | `COMPLETE` |
| 09 Failed PnP | Registration rejection | Failure counted and image listed | `PARTIAL` |
| 10 Insufficient PnP support | Eligibility bound | Solver not called and image listed | `PARTIAL` |
| 11 Low-parallax rejection | Seed guard | Gate C low-parallax/degenerate status | `FAILED` |
| 12 Pure rotation | Translation degeneracy | Relative pose rejected, no camera | `FAILED` |
| 13 Planar degeneracy | Ambiguous seed | Gate C degeneracy, no camera | `FAILED` |
| 14 Far scene | Finite distant geometry | Seed and finite landmarks accepted | `COMPLETE` |
| 15 Disconnected graph | Component discovery | Valid component plus explicit singleton | `PARTIAL` |
| 16 Multiple valid components | Isolation | Two reconstructed components | `COMPLETE` |
| 17 Independent gauges | Per-component gauge | Each seed camera is identity | `COMPLETE` |
| 18 Unregistered images | Explicit output | Remaining image and component key listed | `PARTIAL` |
| 19 Behind-camera landmark | Cheirality | Exact Gate C status and distinct counter | `COMPLETE` model |
| 20 High reprojection error | Residual policy | Finite triangulation then residual rejection | `COMPLETE` model |
| 21 Failed triangulation | Geometry failure | Finite input calls triangulation and fails | `COMPLETE` model |
| 22 Repeated observations | Track coherence | Duplicate image or feature reference rejected | `INVALID_ARGUMENT` |
| 23 Many-camera Track | Landmark lifecycle | One landmark, six ordered observations | `COMPLETE` |
| 24 New landmark after registration | Incremental growth | Ineligible Track accepted after PnP | `COMPLETE` |
| 25 Multi-view growth | All eligible views | New landmark uses at least three views | `COMPLETE` |
| 26 Point refinement | Bounded refinement | Attempt and finite accepted point | `COMPLETE` |
| 27 No-growth termination | Progress bound | One zero-progress round and diagnostic | `PARTIAL` |
| 28 All-images termination | Natural completion | All images registered, no stop diagnostic | `COMPLETE` |
| 29 Seed exhaustion | Candidate bound | Every available candidate attempted | `FAILED` |
| 30 Registration-round exhaustion | Round bound | Exact rounds and remaining images | `PARTIAL` |
| 31 Component ordering | Canonical components | Increasing component keys | success |
| 32 Camera ordering | Canonical cameras | Increasing image IDs | success |
| 33 Landmark ordering | Canonical landmarks | Increasing `(component_key, track_id)` | success |
| 34 In-process repeatability | Local determinism | Complete scientific result equality | same status |
| 35 Fresh-process repeatability | Process determinism | 20 runs emit one signature | same status |
### Gate D validation responsibility
`GATE_D_REQUIRED` covers pointer/count coherence, identities carried by this
API, finite calibration/keypoints, feature-index bounds, Track observation
coherence, geometry failures, atomic result ownership and cleanup. Store-level
Feature Set/File existence is `UPSTREAM_RESPONSIBILITY`: Gate D receives
flattened validated coordinates and never opens a store. Two separate Track
objects with the same ID are `UNREPRESENTABLE_BY_API` because rows are grouped
by `track_id`; duplicate image observations and feature references remain
representable and are rejected. Allocation-failure injection is
`NOT_APPLICABLE_WITH_PROOF`: no allocator injection boundary exists, production
catches allocation failure at the C ABI, and global test allocator state would
violate the architecture.
| Condition | Classification |
|---|---|
| Null parameters, missing arrays, empty input | `GATE_D_REQUIRED` |
| Zero Track/calibration/image/feature identity | `GATE_D_REQUIRED` |
| Missing per-image calibration coverage | `GATE_D_REQUIRED` |
| Zero/non-finite focal or distortion, invalid principal point | `GATE_D_REQUIRED` |
| Invalid feature index or non-finite keypoint | `GATE_D_REQUIRED` |
| Duplicate image/feature observation or singleton Track | `GATE_D_REQUIRED` |
| Seed/PnP/landmark failures and update rollback | `GATE_D_REQUIRED` |
| Missing Feature Set/File in persistent storage | `UPSTREAM_RESPONSIBILITY` |
| Two distinct Track objects sharing one ID | `UNREPRESENTABLE_BY_API` |
| Deterministic allocation-failure injection | `NOT_APPLICABLE_WITH_PROOF` |
The caller retains all input allocations for the synchronous call. The result
owns its arrays; `lardon3d_sparse_incremental_result_destroy()` releases them
and accepts an empty result or null pointer. No C++ exception crosses the C17
boundary.
Count-limit validation uses structurally sufficient fixtures at a lowered
explicit configured limit and proves `LIMIT-1`, `LIMIT`, and `LIMIT+1` without
materializing the public hard maxima. Scientific scale is validated separately
by the small, medium and large resource workloads. Policy tests prove exact
seed-candidate, registration-round and new-landmark-per-round admission; no
policy loop performs a `limit + 1` attempt.
## Out of scope
Beyond the Gate D incremental core, no BA, Project DB integration, metric
alignment, control-point scale, dense/MVS, mesh, texturing, Vulkan SfM, GPU BA,
network/distributed scheduling or UI workflow is implemented here.
## Gate B — model and persistence contract
This section is the **DECISION** contract for the v16 SQL/API work.
It preserves every Gate A decision and supplies only durable vocabulary; no
numeric geometry is introduced.
### Calibration definition and identity
A calibration is an immutable known pinhole model with `width`, `height`,
`fx`, `fy`, `cx`, `cy`, zero skew, `k1`, `k2`, `p1`, and `p2`, all binary64 and
finite. `width` and `height` belong to scientific identity. A calibration's
provenance is an explicit enum (`USER_EXPLICIT` or `IMPORTED_TRUSTED` in v1)
plus a 32-byte provenance fingerprint supplied by the caller. EXIF is never a
calibration origin. Two equal numeric models with different provenance
fingerprints are distinct scientific calibrations because their trust scope is
different; equal content and equal provenance are reused.
The scientific calibration hash is SHA-256 over explicit little-endian fields:
```text
ASCII "L3D3DCP1" (8 bytes)
format_version=1 (uint32)
model_kind (uint32), model_version (uint32)
width (uint32), height (uint32)
fx, fy, cx, cy, k1, k2, p1, p2 (8 canonical binary64 values)
provenance_kind (uint32), provenance_fingerprint (32 bytes)
```
NaN and infinities are rejected. Negative zero is canonicalized to positive
zero before hashing and storage. No native struct, padding or locale text is
serialized. SQLite row IDs remain DB-local references; the hash is the
scientific calibration identity.
### Calibration scope
A scope is immutable and assigns exactly one calibration to each relevant
image. Groups are allowed only when dimensions, crop/orientation coordinate
frame, model/version, numeric parameters and provenance identity are equal;
device or EXIF model names are insufficient. Scope identity is project-local
and content-addressed by SHA-256 over:
```text
ASCII "L3D3DSC1" (8 bytes)
format_version=1 (uint32)
member_count (uint64)
for members sorted by image_id:
image_id (uint64), calibration_hash (32 bytes)
```
The member count is consistency metadata and is also encoded in the digest.
There is no latest-calibration lookup and no silent K rescaling. Feature File
dimensions must match the calibration dimensions exactly.
### Reconstruction identity and components
The immutable reconstruction identity is:
```text
(track_set_id,
calibration_scope_id,
sfm_kind=INCREMENTAL,
sfm_version=1,
parameter_fingerprint[32])
```
`track_set_id` and `calibration_scope_id` are project-local immutable database
references, consistent with the existing Track Model identity convention. The
parameter fingerprint is the raw 32-byte SHA-256 digest of the 372-byte Sparse
SfM parameter record v1 defined above. Gate F materializes it at its
persistent-identity seam; Project DB stores it without owning or recomputing
it. Runtime IDs, timestamps, worker count, resource state and metrics are
excluded.
Component identity is the minimum registered `image_id` in that component. It
is deterministic, project-local, unique because an image belongs to at most one
component, independent of DFS/hash order, and compact. A component always has
at least one registered image. Its coordinates use an independent unit-baseline
gauge and are never comparable to another component without later alignment.
### Persisted model
The minimum v16 model is:
- `sparse_calibrations`: immutable calibration content and hash;
- `sparse_calibration_scopes`: immutable scope hash/member count;
- `sparse_calibration_scope_images`: one image-to-calibration assignment;
- `sparse_reconstructions`: immutable identity and pixel reprojection metrics;
- `sparse_reconstruction_components`: component key and counts;
- `sparse_registered_images`: one world-to-camera pose per image;
- `sparse_landmarks`: one component-local binary64 point per Track;
- `sparse_landmark_observations`: minimal references to the upstream Feature Set
and feature index, without duplicated descriptors or x/y coordinates.
Track ID is globally unique in the existing Track Model table, so one landmark
per reconstruction is uniquely keyed by `(reconstruction_id, track_id)`;
component key remains an attribute/consistency relation rather than redundant
landmark identity. Landmark publication validates that the Track belongs to the
reconstruction's exact Track Set and that every observation belongs to that
Track and its component. Track splitting is impossible in v1.
Observation references are persisted because future BA needs bounded indexed
access from landmark to registered observations without repeatedly reopening
Feature Files. Only `feature_set_id`, `feature_index` and canonical track
position are stored; image ID and x/y remain derivable from immutable upstream
models. This is a deliberate normalization/resource trade-off, not a second
copy of Feature data.
Persisted reprojection metrics use explicit pixel units and names:
`reprojection_rmse_px` and `reprojection_median_px`. They are diagnostics, not
identity. No metric scale or `_mm` field exists.
### Constraints and publication
Publication requires at least two registered images, one component and one
landmark. A result with no usable geometry is rejected rather than represented
by a meaningless empty scientific row. Two-camera reconstruction is valid.
There are no READY/FAILED/PARTIAL scientific states: row existence means a
complete immutable publication. A failed transaction leaves no visible row.
SQL enforces one pose per image, one component per registered image, one
landmark per Track, exact reconstruction uniqueness, scope member uniqueness,
and child foreign keys. The API additionally validates Track Set ownership,
component consistency, calibration coverage, finite values and rotation
orthonormality/determinant. Rotation matrices are never repaired.
The publication transaction inserts the reconstruction and all children using
prepared statements in bounded loops. It contains no Feature File I/O, Track
paging, solver work or Governor wait. Child pages use cursor order and bounded
capacity (64 cameras, 64 landmarks, 64 observations); total scientific counts
have no arbitrary cap beyond checked 64-bit/SQLite limits.
### v16 migration intent
Project DB v15 remains immutable. Gate B adds one transactional v15→v16
migration containing only the eight Sparse SfM model tables and their required
indexes. A true historical v15 fixture, injected rollback, retry, fresh-schema
equivalence and close/reopen are mandatory. No Task, Governor, triangulation,
PnP, BA, GPU or Project DB v17 is introduced.