Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
231d835
fix(packet): qualify study evidence and proposed validation
SamGu-NRX Oct 13, 2026
ec357bb
fix(check): include independent verification in root dispatch
SamGu-NRX Oct 13, 2026
a5e976d
fix(verification): keep simulator evidence tied to its own run
SamGu-NRX Oct 13, 2026
c644217
fix(verification): validate interior footprints and replay failures
SamGu-NRX Oct 13, 2026
853c2df
fix(verification): reject incomplete evidence and stale input hashes
SamGu-NRX Oct 13, 2026
b5da197
fix(recon): preserve evidence and refuse incomplete cached outputs
SamGu-NRX Oct 13, 2026
1194523
fix(recon): rebuild caches with unsupported ZIP compression
SamGu-NRX Oct 13, 2026
7ae364d
test: keep facade tail probes inside wall extents
Oct 14, 2026
10f0fb1
fix: reject ambiguous archives and incomplete acceptance evidence
SamGu-NRX Oct 14, 2026
df7b75d
fix(verification): bound sweep sampling before geometry checks
SamGu-NRX Oct 14, 2026
851814d
fix(pipeline): bound response geometry and preserve capture caches
SamGu-NRX Oct 14, 2026
03b0f0e
docs(recon): distinguish repaired footprint edges from depth policy
SamGu-NRX Oct 14, 2026
2d959c7
docs(recon): describe the remaining battery depth assumption
SamGu-NRX Oct 14, 2026
68cb685
fix(verification): meter actual geometry and comparison work
SamGu-NRX Oct 14, 2026
717e6da
fix(verification): accept wrapped captures and retain alpha checks
SamGu-NRX Oct 14, 2026
67b10e5
fix(verification): reject ambiguous capture scene paths
SamGu-NRX Oct 14, 2026
45645f0
fix(verification): cache scene geometry and bound metadata work
SamGu-NRX Oct 14, 2026
a7b3169
fix(pipeline): verify reported rules and repair encrypted caches
SamGu-NRX Oct 14, 2026
db1e4c2
fix(verification): enforce public policy and validate case metadata
SamGu-NRX Oct 14, 2026
c32ee97
fix(verification): bound diagnostics and harden numeric cache keys
SamGu-NRX Oct 14, 2026
1428d04
fix(verification): preserve Markdown explanations within output budget
SamGu-NRX Oct 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 12 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,35 +1,37 @@
# Suite targets follow their CI jobs where one exists. The viewer has no CI job yet.
# `make check` runs server, web and ios, then scoring, measure-lab, evals, recon
# meter-closeup and the integration viewer where the branch has them.
# meter-closeup, verification and the integration viewer where the branch has them.
# tests/makefile.sh checks this dispatch with stub suites.

XCODEGEN ?= xcodegen
# Must match XCODEGEN_VERSION in .github/workflows/ios.yml; other versions write a different project.
XCODEGEN_VERSION := 2.46.0

# The file each experiment suite's target needs. These suites arrive in separate pull requests, so
# The file each optional suite's target needs. These suites arrive in separate pull requests, so
# `check` includes one only when its file exists and the Makefile works before and after each
# merge. Once all five are on main, list them in `check` directly so a deleted folder fails.
# merge. Once all seven are on main, list them in `check` directly so a deleted folder fails.
SCORING := experiments/scoring/pyproject.toml
MEASURE_LAB := experiments/measure-lab/Makefile
EVALS := experiments/evals/Makefile
RECON := recon/Makefile
METER_CLOSEUP := experiments/meter-closeup/Makefile
VERIFICATION := verification/Makefile
INTEGRATION_VIEWER := tools/integration-viewer/package.json

# $(call present,FILE,TARGET) is TARGET when FILE exists, otherwise nothing.
present = $(if $(wildcard $(1)),$(2))
# $(call require,FILE,PR): stop before any command runs when the suite's file is missing.
require = @test -f $(1) || { echo "make $@: $(1) is missing; the suite arrives with pull request $(2)." >&2; exit 1; }

.PHONY: check server web ios ios-project smoke scoring measure-lab evals recon meter-closeup integration-viewer
.PHONY: check server web ios ios-project smoke scoring measure-lab evals recon meter-closeup verification integration-viewer

check: server web ios \
$(call present,$(SCORING),scoring) \
$(call present,$(MEASURE_LAB),measure-lab) \
$(call present,$(EVALS),evals) \
$(call present,$(RECON),recon) \
$(call present,$(METER_CLOSEUP),meter-closeup) \
$(call present,$(VERIFICATION),verification) \
$(call present,$(INTEGRATION_VIEWER),integration-viewer)

server:
Expand Down Expand Up @@ -94,6 +96,12 @@ meter-closeup:
cd experiments/meter-closeup && uv sync --locked
$(MAKE) -C experiments/meter-closeup check

# Match verification CI's locked sync so a stale lockfile cannot pass the local check.
verification:
$(call require,$(VERIFICATION),#13)
cd verification && uv sync --locked
$(MAKE) -C verification test
Comment thread
SamGu-NRX marked this conversation as resolved.

integration-viewer:
$(call require,$(INTEGRATION_VIEWER),#124)
cd tools/integration-viewer && node --test "test/*.test.js"
Expand Down
14 changes: 8 additions & 6 deletions packet/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,18 +85,20 @@ performance.
6. **Estimated depth with its uncertainty** (S6). A phone without LiDAR can infer metric depth
from the image, but used without its uncertainty it creates surfaces that are not there.
Proposal: a depth `source` of `estimated`, which requires a `sigma` file (float32 meters, one
standard deviation per depth pixel, the depth's layout).
standard deviation per depth pixel, the depth's layout). To keep malformed uncertainty from
reaching geometry calculations, the proposed validator must require exactly `w * h` samples
(`4 * w * h` bytes), all finite and non-negative. A missing, truncated or unreadable map is a
validation problem, not an exception.
7. **The frame of a plane's boundary.** 0.4 lists `planes[].boundary` without saying its frame.
ARKit's `boundaryVertices` are relative to the anchor transform, not to `planeExtent`'s centre
and `rotationOnYAxis`, and packet 1.1 found that mixing the two is an easy mistake. Proposal:
carry the anchor-frame boundary in a new field whose frame is stated, rather than giving the
existing `boundary` a meaning an existing reader might not share, and have the validator check
it lies within `planeExtent` once the centre and rotation are applied.
8. **Photos that frame one span.** With learned depth on ETH3D photos, an along-wall edge came to
0.9 in p90 when seen within 15° of face-on and 24 in beyond 45° (#59,
`experiments/edge-geometry`), so a distance measured inside one face-on photo is likely far
better than one chained across the walk. Proposal: a still or
keyframe purpose `span` that names the marks the photo frames.
8. **Photos that frame one span.** With learned depth on ETH3D electro photos scaled using the
laser reference, in-plane edges had along-wall error of 0.9 in p90 within 15° of face-on and
24 in beyond 45° (#59, `experiments/edge-geometry`). Proposal: a still or keyframe purpose
`span` that names the marks the photo frames.
9. **Scale references in view.** One reference near the meter may not be enough: on MARViN's
walks ARKit's scale varied 1.8% (robust SD) within a walk (#59, `experiments/drift-anatomy`),
so a reference helps where it appears. Proposal: `references[]` with `kind` (door, brick course, and hand-held ones such
Expand Down
3 changes: 2 additions & 1 deletion packet/validator-checks.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ case.
`geometry/mesh.ply` is a binary little-endian PLY. 0.4 does not fix its index and count types, so
this check proposes one layout as well:
- `float x, y, z` per vertex, and per face a `uchar` count followed by that many `int` indices,
then an optional `uchar classification`;
then `uchar classification` when `mesh.classified` is true. The property may be absent only
when the flag is false, so a mesh cannot claim face classes without carrying them;
- the body length equals exactly what the vertex and face counts imply;
- vertex and face counts are non-negative, and every vertex coordinate is finite;
- every face is a triangle, and every index satisfies `0 <= index < vertex_count`;
Expand Down
12 changes: 4 additions & 8 deletions recon/HANDOFF.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,17 +126,13 @@ In `recon/` on this branch:

## 6. Known defects

Reviewers confirmed these in the worker at this branch's head, and none is fixed. Each can let the server settle a check on evidence the worker doesn't have. No replacement below has been built or validated.
These defects remain after the review-debt repairs. They can let the server settle a check on evidence the worker does not have. The saved ETH3D results above predate those repairs; dataset acceptance has not been rerun.

- **One view can count as two for free space.** `recon/coverage.py:183-187, 205-243`: `_slab` treats a voxel as observed when its fused weight is above zero, and any single frame gives it weight. Repro: a capture in which one frame alone sees the space in front of a wall cell reports a facing clearance there. Wall and ground cells need two positions 0.25 m apart for the same claim. The server at 903d86f can settle a check on that clearance. A fix needs visibility kept per frame for free space, with the same two-position test as the wall and ground.
- **The overhead strip is narrower than a battery.** `recon/coverage.py:42, 231-242`: overhead space is sampled only 0.10 to 0.56 m out from the wall front. Repro: an eave 0.05 m out from the wall, or 0.7 m out over a 3 ft deep battery, is not found, and the cell reports clearance above it. A fix needs the footprint of the battery the chosen rule places, passed from the server's rules or request.
- **Old facing and overhead entries survive a small meter move.** `recon/scene.py:50-57, 87-89`: when the meter moves 5 cm or less, the bundle's existing `facing` and `overheads` entries are kept and the new ones appended, although the reconstructed wall's line and orientation replaced the phone's. Repro: a bundle with a phone-measured facing gap, and a reconstructed wall rotated a few degrees about the meter, sends both the old gap and the new one. A fix needs the target wall's earlier entries dropped whenever its line is replaced, as they already are when the meter moves more than 5 cm.
- **One view can count as two for free space.** `recon/coverage.py:189-193, 208-246`: `_slab` treats a voxel as observed when its fused weight is above zero, and any single frame gives it weight. Repro: a capture in which one frame alone sees the space in front of a wall cell reports a facing clearance there. Wall and ground cells need two positions 0.25 m apart for the same claim. The server at 903d86f can settle a check on that clearance. A fix needs visibility kept per frame for free space, with the same two-position test as the wall and ground.
- **Overhead sampling assumes one battery depth.** `recon/coverage.py`: the worker samples both edges of the existing 0.56 m footprint. A 0.7 m eave over a 3 ft deep battery still lies beyond that assumed depth. Other battery depths need the footprint passed from the server's rules or request.
- **The photos-only clearance error is not calibrated.** `recon/pipeline.py:29-35`: facing gaps and overhead clearances from photos carry ±0.56 ft. That comes from the 95% interval of the evals' surface pair-error p90 over 1 to 3 m spans, not from any measurement of where the fused volume puts a boundary. The evals put surface p90 at 5.64 in [4.63, 6.66] and edges at 9.65 in [4.93, 15.01]. Repro: a photos-only overhead clearance measured at 7.10 ft passes the 6.5 ft headroom rule, since 7.10 − 0.56 = 6.54 > 6.5. A fix needs the clearance estimator's own measured uncertainty, or a gate that returns the check unresolved until that exists.

- **Partly occupied space counts as clear.** `recon/coverage.py:223, 235` (threshold at 179): a step out from the wall, or up over the footprint, counts as blocked only when at least 20% of its sampled voxels are occupied, on two steps in a row. That 20% has no source. Otherwise `free_space` reports the space clear through voxels the reconstruction marks occupied. Repro: at 5 cm voxels, a 5 cm rail crossing the facing band (0.3 to 1.8 m high) fills 1 of its 31 height rows, about 3%, and the facing clearance runs straight through it. An obstruction thinner than two steps passes the same way. A fix needs any occupied voxel in the band to end the clear distance or leave the check unresolved.
- **LiDAR without a confidence map is fused unfiltered.** `recon/depth.py:90-92` (accepted at `recon/capture.py:182-189`): a keyframe whose `depth` names a `file` but no `confidenceFile` is accepted, and `lidar()` then keeps every positive sample, including ARKit's low-confidence ones that section 4 says to drop. Repro: a packet keyframe with `"depth": {"file": "k.f32", "w": 256, "h": 192}` has all its samples fused, with no confidence filter. A fix needs a missing confidence map either refused, or treated as that frame having no usable LiDAR.

Two more are filed as issues for the server team: coverage samples ground on a flat plane at the meter's height rather than the fitted slope (#95), and coverage cells can reach past the fitted wall's ends (#96).
The worker now replaces the target wall's prior measurements, stops clearance at any occupied sampled voxel, and refuses LiDAR without a confidence map. Ground coverage follows the fitted slope, and only whole cells within the fitted wall extent are exported. The regressions are in `tests/test_review_debt.py`; they do not validate the remaining clearance policies above.

## 7. Data

Expand Down
18 changes: 17 additions & 1 deletion recon/models/moge_depth.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import math
import os
import sys
import tempfile
from pathlib import Path

DATA = Path(os.environ.get("HOUSE_SCANNING_DATA", Path.home() / "house-scanning-data"))
Expand All @@ -28,6 +29,21 @@
MPS_CAP_GB = 3.6


def write_depth(target: Path, depth) -> None:
import numpy as np

temporary = None
try:
with tempfile.NamedTemporaryFile(dir=target.parent, suffix=".npz", delete=False) as f:
temporary = Path(f.name)
np.savez(f, depth=depth.astype(np.float32))
# A killed writer cannot leave a partially written archive at the reusable cache path.
os.replace(temporary, target)
finally:
if temporary is not None:
temporary.unlink(missing_ok=True)


def main(manifest: Path) -> None:
import cv2
import numpy as np
Expand Down Expand Up @@ -58,7 +74,7 @@ def main(manifest: Path) -> None:
out = model.infer(rgb, resolution_level=RESOLUTION_LEVEL, fov_x=fov_x, use_fp16=True)
depth = out["depth"].float().cpu().numpy()
depth[~out["mask"].cpu().numpy().astype(bool)] = np.nan
np.savez(item["out"], depth=depth.astype(np.float32))
write_depth(Path(item["out"]), depth)
print(f"{Path(item['image']).name}: median {np.nanmedian(depth):.2f} m", file=sys.stderr)
del out, rgb
if device == "mps":
Expand Down
Loading