Skip to content

Commit a95a9d2

Browse files
DTTerastarclaude
andcommitted
Tomography: spatial-smoothness reconstruction (interpolate between links)
Replace the per-cell magnitude prior (independent cells → streaks along link lines) with a Laplacian smoothness prior: min ||Wx-y||^2 + λ·Σ_neighbours (x_i-x_j)^2 + ε||x||^2. The field now interpolates smoothly between links (contiguous blobs, no streaking) while the tiny ε ridge keeps genuinely unobserved far cells at zero — interpolation grounded in physics, not invented data. 2D overlay keeps a mild coverage fade as a confidence cue. (3D view explored and dropped — 2D heatmap reads better.) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 8a3b5e7 commit a95a9d2

3 files changed

Lines changed: 41 additions & 13 deletions

File tree

src/Services/RadioTomographyService.cs

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,10 @@ public class RadioTomographyService : BackgroundService
2020
// Tunables (kept conservative for a first cut).
2121
private const double FreeSpaceExponent = 2.0; // n in rssi = ref - 10*n*log10(d)
2222
private const double CellSizeMeters = 1.0;
23-
private const int MaxCells = 1200; // bound the inverse-problem size
23+
private const int MaxCells = 1200; // bound the inverse-problem size (Cholesky is O(cells^3))
2424
private const int MinLinksPerFloor = 6;
25-
private const double Regularization = 0.15; // ridge strength, relative to data scale
25+
private const double SmoothLambda = 0.4; // spatial-smoothness strength, relative to data scale
26+
private const double RidgeEpsilon = 0.01; // tiny magnitude prior: pulls truly-unobserved cells to 0
2627
private const double DecayDbPerCycle = 1.0; // how fast the per-link "clean" peak forgets
2728
private static readonly TimeSpan Interval = TimeSpan.FromSeconds(30);
2829

@@ -119,7 +120,7 @@ private TomographyResult Compute()
119120

120121
if (rows_W.Count < MinLinksPerFloor) return null;
121122

122-
var attenuation = SolveRidge(rows_W, ys, cellCount);
123+
var attenuation = SolveSmooth(rows_W, ys, cols, rows);
123124

124125
var tf = new TomographyFloor
125126
{
@@ -170,22 +171,48 @@ private static void RasterizeRay(double x0, double y0, double x1, double y1,
170171
}
171172
}
172173

173-
/// <summary>Non-negative ridge regression: min ||Wx - y||^2 + λ||x||^2, then clamp x >= 0.</summary>
174-
private static double[] SolveRidge(List<double[]> rowsW, List<double> ys, int cellCount)
174+
/// <summary>
175+
/// Smoothness-regularized non-negative reconstruction:
176+
/// min ||Wx - y||^2 + λ·Σ_neighbours (x_i - x_j)^2 + ε||x||^2, then clamp x >= 0.
177+
/// The Laplacian term couples adjacent cells so the field interpolates smoothly between links
178+
/// (no streaking, contiguous blobs) instead of treating every cell independently. The tiny ε
179+
/// ridge fixes the Laplacian's constant null space and pulls genuinely unobserved cells to 0.
180+
/// </summary>
181+
private static double[] SolveSmooth(List<double[]> rowsW, List<double> ys, int cols, int rows)
175182
{
183+
int cellCount = cols * rows;
176184
int L = rowsW.Count;
177185
var W = Matrix<double>.Build.Dense(L, cellCount, (i, j) => rowsW[i][j]);
178186
var y = Vector<double>.Build.Dense(L, i => ys[i]);
179187

180-
var wtw = W.TransposeThisAndMultiply(W); // C x C, SPD after ridge
188+
var A = W.TransposeThisAndMultiply(W); // C x C
181189
double meanDiag = 0;
182-
for (int i = 0; i < cellCount; i++) meanDiag += wtw[i, i];
190+
for (int i = 0; i < cellCount; i++) meanDiag += A[i, i];
183191
meanDiag = cellCount > 0 ? meanDiag / cellCount : 1.0;
184-
double lambda = Regularization * meanDiag + 1e-9;
185-
for (int i = 0; i < cellCount; i++) wtw[i, i] += lambda;
192+
double lambda = SmoothLambda * meanDiag;
193+
double eps = RidgeEpsilon * meanDiag + 1e-9;
194+
195+
// Add λ·(graph Laplacian over 4-neighbours): x^T L x = Σ_edges (x_i - x_j)^2.
196+
for (int r = 0; r < rows; r++)
197+
for (int c = 0; c < cols; c++)
198+
{
199+
int idx = r * cols + c;
200+
void Edge(int nr, int nc)
201+
{
202+
if (nr < 0 || nr >= rows || nc < 0 || nc >= cols) return;
203+
int nidx = nr * cols + nc;
204+
A[idx, idx] += lambda;
205+
A[idx, nidx] -= lambda;
206+
}
207+
Edge(r - 1, c);
208+
Edge(r + 1, c);
209+
Edge(r, c - 1);
210+
Edge(r, c + 1);
211+
}
212+
for (int i = 0; i < cellCount; i++) A[i, i] += eps;
186213

187214
var wty = W.TransposeThisAndMultiply(y);
188-
var x = wtw.Cholesky().Solve(wty);
215+
var x = A.Cholesky().Solve(wty);
189216

190217
var result = new double[cellCount];
191218
for (int i = 0; i < cellCount; i++) result[i] = Math.Max(0, x[i]);

src/ui/src/lib/Tomography.svelte

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,9 @@
4848
const cx = tomo.minX + col * tomo.cellSize;
4949
const cy = tomo.minY + row * tomo.cellSize;
5050
const intensity = Math.min(1, att / maxAtt);
51-
// Fade cells that few links cross — low confidence, not "nothing there".
52-
const conf = Math.min(1, cov / (0.25 * maxCov));
51+
// Smoothness interpolates between links; keep interpolated cells visible but still
52+
// fade the lowest-coverage areas a little as a confidence cue (floored at 0.45).
53+
const conf = 0.45 + 0.55 * Math.min(1, cov / (0.25 * maxCov));
5354
const r = rect(cx, cy, tomo.cellSize);
5455
out.push({
5556
...r,

src/ui/src/routes/calibration/+page.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
{:else if calibrationType === 'obstructions'}
3232
<section class="px-4 space-y-2">
3333
<p class="text-sm text-surface-600-300">
34-
Static RF-attenuation map reconstructed from node-to-node links (radio tomography). Hot cells are where the radio sees something solid — walls, appliances, a refrigerator. This is a sanity check: if the hot blobs line up with things you know are there, the model is learning real physics. Updates every ~30s; sparse-coverage areas read cooler than reality.
34+
Static RF-attenuation map reconstructed from node-to-node links (radio tomography). Hot cells are where the radio sees something solid — walls, appliances, a refrigerator. If the hot spots line up with things you know are there, the model is learning real physics. Updates every ~30s; sparse-coverage areas read cooler than reality.
3535
</p>
3636
<FloorTabs bind:floorId />
3737
<div class="w-full" style="height: 70vh">

0 commit comments

Comments
 (0)