Compare commits

..

1 Commits

Author SHA1 Message Date
github-actions[bot] 23f2cc7746 chore: update vendor submodules to latest upstream 2026-06-01 07:58:06 +00:00
15 changed files with 25 additions and 236 deletions
-2
View File
@@ -7,7 +7,6 @@ on:
- 'archive/v1/src/core/**'
- 'archive/v1/src/hardware/**'
- 'archive/v1/data/proof/**'
- 'archive/v1/requirements-lock.txt'
- '.github/workflows/verify-pipeline.yml'
pull_request:
branches: [ main, master ]
@@ -15,7 +14,6 @@ on:
- 'archive/v1/src/core/**'
- 'archive/v1/src/hardware/**'
- 'archive/v1/data/proof/**'
- 'archive/v1/requirements-lock.txt'
- '.github/workflows/verify-pipeline.yml'
workflow_dispatch:
@@ -1 +1 @@
f8e76f21a0f9852b70b6d9dd5318239f6b20cbcb4cdd995863263cecdc446f7a
ca58956c1bbee8c46f1798b3d6b6f1f829aa5db90bba53e07177830eca429199
+16 -148
View File
@@ -185,14 +185,7 @@ def frame_to_csi_data(frame, signal_meta):
# observed pipeline-amplified ULP drift and is still far below any meaningful
# signal change (CSI phase precision is ~1e-3 rad; PSD bins differ by orders
# of magnitude). Round to this precision, then hash.
#
# NOTE: 6 decimals collapses the divergence *across Linux microarchitectures*
# but NOT Windows-vs-Linux, where the pocketfft/BLAS difference exceeds 1e-6 on
# a few elements that then straddle the 6th-decimal rounding boundary. The
# precision is overridable via PROOF_HASH_DECIMALS so it can be coarsened to a
# value that is boundary-stable across *all* platforms (Windows + Linux + macOS)
# while staying far below any signal-meaningful change.
HASH_QUANTIZATION_DECIMALS = int(os.environ.get("PROOF_HASH_DECIMALS", "6"))
HASH_QUANTIZATION_DECIMALS = 6
def features_to_bytes(features):
@@ -212,20 +205,13 @@ def features_to_bytes(features):
"""
parts = []
# Serialize each feature array in declaration order.
# doppler_shift is INTENTIONALLY excluded: it is peak-normalized
# (`spectrum / max(spectrum)` in csi_processor._extract_doppler_features),
# and when the raw spectrum has near-tied peaks the argmax flips under
# cross-microarchitecture FP reordering, renormalizing the whole array
# (O(1) divergence — not absorbable by any tolerance). The remaining five
# features, including the FFT-based PSD, reproduce deterministically and
# provide the proof. (The underlying doppler instability is a production
# reproducibility bug tracked separately.)
# Serialize each feature array in declaration order
for array in [
features.amplitude_mean,
features.amplitude_variance,
features.phase_difference,
features.correlation_matrix,
features.doppler_shift,
features.power_spectral_density,
]:
flat = np.asarray(array, dtype=np.float64).ravel()
@@ -239,45 +225,6 @@ def features_to_bytes(features):
return b"".join(parts)
# ── Cross-platform tolerance gate (issue #560 follow-up) ─────────────────────
# The SHA-256 of fixed-decimal-rounded features is bit-exact only WITHIN one
# CPU microarchitecture. The pocketfft / BLAS kernels in the manylinux
# numpy/scipy wheels reorder floating-point reductions differently across
# microarchs (e.g. a GitHub Azure runner vs a developer box vs another Linux
# host), and the resulting ~1e-6 *relative* drift lands on large-magnitude PSD
# bins as an absolute difference too large for ANY fixed-decimal grid to absorb
# (empirically the hash diverges across microarchs even at 2 decimals). So:
# • the hash is the strong, bit-exact, SAME-platform proof, and
# • a relative tolerance against a committed reference vector is the
# platform-INDEPENDENT proof.
# A run PASSES if either matches. Tolerances sit ~100x over the observed
# microarch drift and ~10x under any signal-meaningful change (CSI phase
# precision ~1e-3 rad), so real pipeline regressions still fail.
TOLERANCE_RTOL = 1e-4
TOLERANCE_ATOL = 1e-6
REFERENCE_VECTOR_FILENAME = "expected_features_reference.npz"
def features_to_vector(features):
"""Concatenate a frame's feature arrays as raw float64 (no rounding).
Mirrors ``features_to_bytes`` ordering but keeps full precision, for the
tolerance-based cross-platform comparison.
"""
# doppler_shift excluded — see features_to_bytes for the rationale
# (peak-normalization argmax instability across CPU microarchitectures).
arrays = [
features.amplitude_mean,
features.amplitude_variance,
features.phase_difference,
features.correlation_matrix,
features.power_spectral_density,
]
return np.concatenate(
[np.asarray(a, dtype=np.float64).ravel() for a in arrays]
)
def compute_pipeline_hash(data_path, verbose=False):
"""Run the full pipeline and compute the SHA-256 hash of all features.
@@ -320,7 +267,6 @@ def compute_pipeline_hash(data_path, verbose=False):
features_count = 0
total_feature_bytes = 0
last_features = None
feature_vectors = []
doppler_nonzero_count = 0
doppler_shape = None
psd_shape = None
@@ -337,7 +283,6 @@ def compute_pipeline_hash(data_path, verbose=False):
if features is not None:
feature_bytes = features_to_bytes(features)
hasher.update(feature_bytes)
feature_vectors.append(features_to_vector(features))
features_count += 1
total_feature_bytes += len(feature_bytes)
last_features = features
@@ -406,11 +351,7 @@ def compute_pipeline_hash(data_path, verbose=False):
"psd_shape": psd_shape,
}
reference_vector = (
np.concatenate(feature_vectors) if feature_vectors else np.array([], dtype=np.float64)
)
return hasher.hexdigest(), reference_vector, stats
return hasher.hexdigest(), stats
def audit_codebase(base_dir=None):
@@ -526,7 +467,7 @@ def main():
print(" This runs the SAME CSIProcessor.preprocess_csi_data() and")
print(" CSIProcessor.extract_features() used in production.")
print()
computed_hash, computed_vector, stats = compute_pipeline_hash(data_path, verbose=args.verbose)
computed_hash, stats = compute_pipeline_hash(data_path, verbose=args.verbose)
# ---------------------------------------------------------------
# Step 3: Hash comparison
@@ -538,11 +479,8 @@ def main():
with open(hash_path, "w") as f:
f.write(computed_hash + "\n")
print(f" Wrote expected hash to {hash_path}")
ref_path = os.path.join(SCRIPT_DIR, REFERENCE_VECTOR_FILENAME)
np.savez_compressed(ref_path, features=computed_vector)
print(f" Wrote reference vector ({computed_vector.size} values) to {ref_path}")
print()
print(" HASH + REFERENCE GENERATED -- run without --generate-hash to verify.")
print(" HASH GENERATED -- run without --generate-hash to verify.")
print("=" * 72)
return
@@ -561,70 +499,13 @@ def main():
print(f" Expected: {expected_hash}")
hash_match = computed_hash == expected_hash
# Cross-platform fallback: if the bit-exact hash differs (different CPU
# microarchitecture reorders the pocketfft/BLAS reductions), accept the run
# when the raw feature vector matches the committed reference within a
# relative tolerance — platform-independent where the hash is not (#560).
tolerance_match = False
max_abs_dev = None
max_rel_dev = None
ref_path = os.path.join(SCRIPT_DIR, REFERENCE_VECTOR_FILENAME)
if not hash_match and os.path.exists(ref_path):
ref_vec = np.load(ref_path)["features"]
if ref_vec.shape == computed_vector.shape:
tolerance_match = bool(
np.allclose(
computed_vector, ref_vec, rtol=TOLERANCE_RTOL, atol=TOLERANCE_ATOL
)
)
diff = np.abs(computed_vector - ref_vec)
max_abs_dev = float(np.max(diff)) if diff.size else 0.0
max_rel_dev = (
float(np.max(diff / np.maximum(np.abs(ref_vec), 1e-12)))
if diff.size
else 0.0
)
if hash_match:
match_status = "MATCH (bit-exact)"
elif tolerance_match:
match_status = f"TOLERANCE MATCH (max rel dev {max_rel_dev:.2e})"
if computed_hash == expected_hash:
match_status = "MATCH"
else:
match_status = "MISMATCH"
print(f" Status: {match_status}")
print()
if not hash_match and max_abs_dev is not None:
block_sizes = [56, 56, 55, 9, 128] # per-frame feature layout (doppler excluded)
block_names = ["amp_mean", "amp_var", "phase_diff", "corr", "psd"]
frame_len = sum(block_sizes)
tol = TOLERANCE_ATOL + TOLERANCE_RTOL * np.abs(ref_vec)
outside = diff > tol
n_out = int(outside.sum())
print(
f" DIVERGENCE: {n_out}/{computed_vector.size} outside tol "
f"({100.0 * n_out / computed_vector.size:.4f}%) "
f"max|d|={max_abs_dev:.3e} maxrel={max_rel_dev:.3e}"
)
if n_out:
wf = np.where(outside)[0] % frame_len
bounds = np.cumsum([0] + block_sizes)
parts = []
for bi, name in enumerate(block_names):
c = int(((wf >= bounds[bi]) & (wf < bounds[bi + 1])).sum())
if c:
parts.append(f"{name}={c}")
print(f" by feature: {', '.join(parts)}")
for w in np.argsort(diff)[::-1][:4]:
b = int(np.searchsorted(bounds, int(w) % frame_len, side="right")) - 1
print(
f" worst idx {int(w)} ({block_names[b]}): "
f"ref={ref_vec[int(w)]:.6g} got={computed_vector[int(w)]:.6g}"
)
print()
# ---------------------------------------------------------------
# Step 4: Audit (if requested or always in full mode)
# ---------------------------------------------------------------
@@ -647,22 +528,14 @@ def main():
# Final verdict
# ---------------------------------------------------------------
print("=" * 72)
if hash_match or tolerance_match:
if computed_hash == expected_hash:
print(" VERDICT: PASS")
print()
if hash_match:
print(" The pipeline produced a SHA-256 hash that matches the published")
print(" expected hash (bit-exact). This proves:")
else:
print(" The bit-exact hash differs (CPU-microarchitecture FP reordering),")
print(" but the raw feature vector matches the published reference within")
print(
f" rtol={TOLERANCE_RTOL:g} / atol={TOLERANCE_ATOL:g} "
f"(max rel dev {max_rel_dev:.2e}). This proves:"
)
print(" The pipeline produced a SHA-256 hash that matches the published")
print(" expected hash. This proves:")
print(" 1. The SAME signal processing code ran on the reference signal")
print(" 2. The output is DETERMINISTIC (same input -> same output)")
print(" 3. No randomness was introduced")
print(" 3. No randomness was introduced (hash would differ)")
print(" 4. The code path includes: noise removal, Hamming windowing,")
print(" amplitude normalization, FFT-based Doppler extraction,")
print(" and power spectral density computation")
@@ -673,19 +546,14 @@ def main():
else:
print(" VERDICT: FAIL")
print()
print(" The pipeline output does NOT match the expected hash OR the")
print(" reference feature vector within tolerance.")
if max_rel_dev is not None:
print(
f" max abs dev: {max_abs_dev:.3e} max rel dev: {max_rel_dev:.3e}"
f" (rtol={TOLERANCE_RTOL:g}, atol={TOLERANCE_ATOL:g})"
)
print(" The pipeline output does NOT match the expected hash.")
print()
print(" Possible causes:")
print(" - Numpy/scipy version mismatch (check requirements)")
print(" - Code change in CSI processor that alters numerical output")
print(" - A real (non-microarch) numerical regression")
print(" - Platform floating-point differences (unlikely for IEEE 754)")
print()
print(" To update after an intentional change:")
print(" To update the expected hash after intentional changes:")
print(" python verify.py --generate-hash")
print("=" * 72)
sys.exit(1)
+2 -8
View File
@@ -6,14 +6,8 @@
#
# To update: change versions, run `python v1/data/proof/verify.py --generate-hash`,
# then commit the new expected_features.sha256.
#
# numpy/scipy track the versions the *published* expected hash
# (expected_features.sha256 = ca58956c…) was generated with — modern numpy 2.x,
# i.e. what a fresh `pip install numpy` and the proof-of-capabilities.md skeptic
# path produce today. The old 1.26.4 pin no longer matched that hash and made
# the determinism gate fail against its own published proof.
numpy==2.4.2
scipy==1.17.1
numpy==1.26.4
scipy==1.14.1
pydantic==2.10.4
pydantic-settings==2.7.1
+2 -9
View File
@@ -78,18 +78,11 @@ random or mocked, the hash would not be reproducible.
```bash
python archive/v1/data/proof/verify.py
# Expect: VERDICT: PASS
# Pipeline hash: f8e76f21a0f9852b70b6d9dd5318239f6b20cbcb4cdd995863263cecdc446f7a
# Pipeline hash: ca58956c1bbee8c46f1798b3d6b6f1f829aa5db90bba53e07177830eca429199
```
The published expected hash is committed at `archive/v1/data/proof/expected_features.sha256`.
Run it on your machine — it reproduces **bit-for-bit across platforms** (verified identical on
Windows, two independent Linux hosts, and the GitHub Azure CI runner). For the one feature that
*isn't* bit-stable — the peak-normalized Doppler spectrum, whose argmax flips under
cross-microarchitecture FFT reordering — the proof excludes it from the hash and additionally
checks every other feature against a committed reference vector within a strict relative tolerance
(`expected_features_reference.npz`), so a genuine regression still fails while CPU-level float
noise does not. Five features (amplitude mean/variance, phase difference, correlation matrix, and
the FFT-based PSD) carry the deterministic proof.
Run it on your machine; the hash must match bit-for-bit.
**On the "fake data" allegation specifically:** the reference signal is *deliberately
synthetic* and **labels itself as such**`archive/v1/data/proof/sample_csi_meta.json` says:
@@ -637,23 +637,6 @@ static void hop_timer_cb(void *arg)
csi_hop_next_channel();
}
void csi_collector_enable_data_capture(void)
{
/* MGMT-only (RuView#396) starves the CSI callback on display-less boards
* (RuView#521/#893): beacons alone are sparse, yield collapses to 0 pps.
* Without a display there is no QSPI/SPI-flash cache contention with the
* DATA-frame interrupt load, so capture DATA frames too. */
wifi_promiscuous_filter_t filt = {
.filter_mask = WIFI_PROMIS_FILTER_MASK_MGMT | WIFI_PROMIS_FILTER_MASK_DATA,
};
esp_err_t err = esp_wifi_set_promiscuous_filter(&filt);
if (err == ESP_OK) {
ESP_LOGI(TAG, "CSI filter upgraded to MGMT+DATA (no display, RuView#893)");
} else {
ESP_LOGW(TAG, "Failed to enable DATA-frame CSI capture: %s", esp_err_to_name(err));
}
}
void csi_collector_start_hop_timer(void)
{
if (s_hop_count <= 1) {
@@ -90,19 +90,6 @@ void csi_hop_next_channel(void);
*/
void csi_collector_start_hop_timer(void);
/**
* Upgrade the promiscuous filter to capture DATA frames in addition to MGMT
* (RuView#893/#521).
*
* Called on display-less boards: the MGMT-only filter (the #396 display-crash
* workaround set in csi_collector_init) only fires the CSI callback on sparse
* management frames, so yield collapses to 0 pps under real traffic and the
* node looks dead. A board with no AMOLED panel has no QSPI/SPI-flash cache
* contention, so it can safely capture DATA frames — restoring abundant CSI.
* Display boards keep MGMT-only to avoid the #396 crash.
*/
void csi_collector_enable_data_capture(void);
/**
* Inject an NDP (Null Data Packet) frame for sensing.
*
@@ -9,14 +9,6 @@
#include "display_task.h"
#include "sdkconfig.h"
/* Set true once an AMOLED panel is detected and the display task starts.
* Defined outside the CONFIG_DISPLAY_ENABLE guard so display_is_active()
* exists on headless builds too (where it stays false → CSI captures DATA
* frames; see RuView#893). */
static bool s_display_active = false;
bool display_is_active(void) { return s_display_active; }
#if CONFIG_DISPLAY_ENABLE
#include <string.h>
@@ -170,7 +162,6 @@ esp_err_t display_task_start(void)
ESP_LOGI(TAG, "Display task started (Core %d, priority %d, %d fps)",
DISP_TASK_CORE, DISP_TASK_PRIORITY, DISP_FPS_LIMIT);
s_display_active = true;
return ESP_OK;
}
@@ -7,7 +7,6 @@
#define DISPLAY_TASK_H
#include "esp_err.h"
#include <stdbool.h>
#ifdef __cplusplus
extern "C" {
@@ -23,15 +22,6 @@ extern "C" {
*/
esp_err_t display_task_start(void);
/**
* @return true once an AMOLED panel has been detected and the display task
* is running; false on headless boards (no panel, or built without display
* support). Used to choose the CSI promiscuous filter (RuView#893): a board
* with no display has no QSPI/SPI-flash contention, so it can safely capture
* DATA frames for proper CSI yield instead of starving on MGMT-only.
*/
bool display_is_active(void);
#ifdef __cplusplus
}
#endif
-15
View File
@@ -410,21 +410,6 @@ void app_main(void)
}
#endif
/* RuView#893/#521: the MGMT-only promiscuous filter (set in
* csi_collector_init as the #396 display-crash workaround) starves the CSI
* callback on display-less boards — yield collapses to 0 pps and the node
* looks dead despite being on the network. Now that the display probe has
* run, boards with no AMOLED panel (no QSPI/SPI-flash cache contention)
* upgrade the filter to capture DATA frames too, restoring CSI yield. */
#ifdef CONFIG_DISPLAY_ENABLE
bool has_display = display_is_active(); /* runtime panel probe result */
#else
bool has_display = false; /* display support not compiled in */
#endif
if (!has_display) {
csi_collector_enable_data_capture();
}
ESP_LOGI(TAG, "CSI streaming active → %s:%d (edge_tier=%u, OTA=%s, WASM=%s, mmWave=%s, swarm=%s, adapt=%s)",
g_nvs_config.target_ip, g_nvs_config.target_port,
g_nvs_config.edge_tier,
+1 -1