Experiment 015 — Uncertainty Bridge: Sensor Noise → Localization

Rendered from exp-015-uncertainty-bridge.ipynb

Experiment 015 — Uncertainty Bridge: Sensor Noise → Localization

Propagates sensor measurement noise (Exp 001) through Anderson localization (Exp 008) to predict how soil moisture sensor accuracy affects quorum sensing regime predictions. Pipeline: θ_measured = θ_true + bias + N(0, σ) (Exp 001: sensor noise) W_eff = α * θ + β (moisture → disorder mapping) γ = lyapunov_exponent(W_eff, E=0) (Exp 008: Anderson model) ξ = 1/γ (localization length) Key question: How much does sensor noise in θ propagate into uncertainty in ξ (the QS signal propagation length)? Is bias correction sufficient to reduce this uncertainty below a useful threshold? Data sources:

  • Dong et al. (2020) sensor calibration (Exp 001 benchmark)
  • Anderson localization analytical model (Exp 008)
  • No external data — fully analytical + Monte Carlo

Domain: Cross Domain Faculty: Cross-domain bridge Reference: Cross-domain error propagation

Data source: control/uncertainty_bridge/uncertainty_bridge.py + benchmark_*.json


This notebook is the publication-grade Python baseline for Experiment 015. The identical computations are validated in Rust (see validate_* binary) and delegated to barraCuda for GPU acceleration.

import json
import math
import sys
from pathlib import Path

import numpy as np
import matplotlib.pyplot as plt

# Wire path to groundSpring control/ for common utilities
CONTROL = Path('..') / '..' / 'control'
sys.path.insert(0, str(CONTROL))
from common import *  # noqa: F403 — validation harness

# Load benchmark data
benchmark_path = CONTROL / 'uncertainty_bridge' / 'benchmark_uncertainty_bridge.json'
with open(benchmark_path) as f:
    benchmark = json.load(f)

PASS_COLOR = '#2ecc71'
FAIL_COLOR = '#e74c3c'
INFO_COLOR = '#3498db'

print(f'Loaded benchmark: benchmark_uncertainty_bridge.json')
print(f'Provenance: {benchmark.get("_provenance", {})}')

Validation

Initialization

print("groundSpring Exp 015: Uncertainty Bridge")
print("  Sensor noise → Anderson localization → QS regime uncertainty")

sensor = benchmark["sensor_noise"]
anderson = benchmark["anderson_model"]
prop = benchmark["propagation"]
expected = benchmark["expected"]

chain_length = anderson["chain_length"]
n_real = anderson["n_realizations"]
n_mc = prop["n_mc_samples"]
slope = prop["theta_to_disorder_slope"]
intercept = prop["theta_to_disorder_intercept"]
theta_nom = prop["theta_nominal"]

rng = np.random.default_rng(prop.get("mc_seed", 2026))

Anderson model sanity checks

for w in anderson["disorder_range"]:
    gamma = lyapunov_averaged(w, 0.0, chain_length, n_real, 42)
    print(f"  W={w:5.1f} → γ={gamma:.4f}, ξ={1.0/max(gamma,1e-10):.1f}")

gammas = [
    lyapunov_averaged(w, 0.0, chain_length, n_real, 42)
    for w in anderson["disorder_range"]
]
from itertools import pairwise
monotonic = all(g1 <= g2 for g1, g2 in pairwise(gammas))
check_true("Lyapunov exponent monotonically increasing with W", monotonic)

check_true(
    "Clean system (W=0.5) has small γ",
    gammas[0] < 0.1,
)
check_true(
    "Strong disorder (W=12) has large γ",
    gammas[-1] > 0.3,
)

CS616 Sand sensor noise propagation

cs616 = sensor["cs616_sand"]

cs616_raw = propagate_sensor_noise(
    theta_nom, cs616["bias_mbe"], cs616["random_sigma"],
    slope, intercept, chain_length, n_real, n_mc, rng,
)
print(f"  Raw:  ξ = {cs616_raw['xi_mean']:.1f} ± {cs616_raw['xi_std']:.1f} "
      f"(CV = {cs616_raw['xi_cv']:.3f})")

cs616_corrected = propagate_bias_corrected(
    theta_nom, cs616["bias_mbe"], cs616["random_sigma"],
    slope, intercept, chain_length, n_real, n_mc, rng,
)
print(f"  Corrected: ξ = {cs616_corrected['xi_mean']:.1f} ± "
      f"{cs616_corrected['xi_std']:.1f} (CV = {cs616_corrected['xi_cv']:.3f})")

check_range(
    "CS616 localization length CV",
    cs616_raw["xi_cv"],
    expected["localization_length_cv_cs616"]["min"],
    expected["localization_length_cv_cs616"]["max"],
)

EC5 Sandy Clay Loam sensor noise propagation

ec5 = sensor["ec5_sandy_clay_loam"]

ec5_raw = propagate_sensor_noise(
    theta_nom, ec5["bias_mbe"], ec5["random_sigma"],
    slope, intercept, chain_length, n_real, n_mc, rng,
)
print(f"  Raw:  ξ = {ec5_raw['xi_mean']:.1f} ± {ec5_raw['xi_std']:.1f} "
      f"(CV = {ec5_raw['xi_cv']:.3f})")

ec5_corrected = propagate_bias_corrected(
    theta_nom, ec5["bias_mbe"], ec5["random_sigma"],
    slope, intercept, chain_length, n_real, n_mc, rng,
)
print(f"  Corrected: ξ = {ec5_corrected['xi_mean']:.1f} ± "
      f"{ec5_corrected['xi_std']:.1f} (CV = {ec5_corrected['xi_cv']:.3f})")

check_range(
    "EC5 localization length CV",
    ec5_raw["xi_cv"],
    expected["localization_length_cv_ec5"]["min"],
    expected["localization_length_cv_ec5"]["max"],
)

Cross-sensor comparison

check_true(
    "EC5 has higher CV than CS616 (more noise → more uncertainty)",
    ec5_raw["xi_cv"] > cs616_raw["xi_cv"],
)

Bias correction effectiveness

ec5_improvement = 1.0 - ec5_corrected["xi_cv"] / max(ec5_raw["xi_cv"], 1e-10)
print(f"  EC5 CV reduction from bias correction: {ec5_improvement:.1%}")

min_reduction = expected["bias_corrected_improvement"]["min_reduction_fraction"]
check_min(
    "EC5 bias correction reduces CV",
    ec5_improvement,
    min_reduction,
)

cs616_improvement = 1.0 - cs616_corrected["xi_cv"] / max(cs616_raw["xi_cv"], 1e-10)
print(f"  CS616 CV reduction from bias correction: {cs616_improvement:.1%}")

check_true(
    "EC5 benefits more from bias correction than CS616 (higher bias fraction)",
    ec5_improvement > cs616_improvement or cs616["bias_fraction"] < ec5["bias_fraction"],
)

Summary

print("\n" + "=" * 72)
print("Uncertainty Bridge Summary:")
print(f"  CS616 Sand:         CV(ξ) = {cs616_raw['xi_cv']:.3f}"
      f"{cs616_corrected['xi_cv']:.3f} (corrected)")
print(f"  EC5 Sandy Clay Loam: CV(ξ) = {ec5_raw['xi_cv']:.3f}"
      f"{ec5_corrected['xi_cv']:.3f} (corrected)")
print("  Sensor ranking preserved: EC5 > CS616 in uncertainty")
print(f"  Bias correction: EC5 improves {ec5_improvement:.0%}, "
      f"CS616 improves {cs616_improvement:.0%}")

print_summary("Exp 015: Uncertainty Bridge")
return 1 if fail_count() > 0 else 0

Visualization

# Publication-grade summary chart for Exp 015
fig, ax = plt.subplots(figsize=(8, 4))

p, f_count, t = pass_count(), fail_count(), total_count()
ax.barh(['Pass', 'Fail'], [p, f_count], color=[PASS_COLOR, FAIL_COLOR])
ax.set_xlim(0, max(t * 1.15, 1))
ax.set_title('Exp 015: Uncertainty Bridge: Sensor Noise → Localization — Validation Results')
ax.set_xlabel('Check Count')
for i, v in enumerate([p, f_count]):
    if v > 0:
        ax.text(v + 0.3, i, str(v), va='center', fontweight='bold')

plt.tight_layout()
plt.savefig(f'/tmp/groundspring_exp015.png', dpi=150, bbox_inches='tight')
plt.show()
print(f'\nResult: {p}/{t} PASS, {f_count}/{t} FAIL')

Provenance & Summary

FieldValue
Experiment015 — Uncertainty Bridge: Sensor Noise → Localization
DomainCross Domain
ReferenceCross-domain error propagation
FacultyCross-domain bridge
Python baselinecontrol/uncertainty_bridge/uncertainty_bridge.py
Benchmark JSONcontrol/uncertainty_bridge/benchmark_uncertainty_bridge.json
Rust validatorvalidate_* binary (exit-code protocol)
Rust speedupSee benchmark comparison notebook
LicenseAGPL-3.0-or-later

Provenance chain: Python baseline → Rust validation → barraCuda GPU → metalForge cross-substrate → primal IPC composition

See primals.eco for rendered lab notebooks.