File size: 11,077 Bytes
dadad9c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
---
license: apache-2.0
library_name: ir-source-localizer
tags:
  - acoustics
  - localization
  - impulse-response
  - ransac
  - numerical
  - numpy
  - cpu
pipeline_tag: other
language:
  - en
---

# ir-source-localizer

**Given impulse responses at known microphones, return the
source's (x, y) position to sub-centimetre accuracy.**

Four microphones in the corners of a shoebox room. A source emits
a short click. Four IRs are recorded. Each IR's direct-arrival
time gives one constraint on the source position. Four
constraints, two unknowns — overdetermined and solvable. RANSAC
over receiver subsets rejects outlier detections when one of the
four arrivals is unreliable.

Result from the synthetic demo: **0.28 cm error at 2% noise**,
stable from 0.5% to 10% noise. One receiver is systematically off
by 0.70 ms (the geometry gives it an ambiguous direct-vs-reflection
mixture); RANSAC identifies it and drops it, leaving three
sub-sample-accurate arrivals.

## The claim in one sentence

Four cheap microphones and a click, in an ordinary room, locate
the click to within a centimetre.

## What it produces

A `LocalizationResult` containing:

- `x`, `y` — source position in metres
- `sigma_x`, `sigma_y` — 1-sigma uncertainty from Monte Carlo
- `detected_times`, `predicted_times` — per-receiver arrival times
- `arrival_residual_ms` — predicted minus detected, per receiver
- `detection_quality` — per-receiver envelope confidence
- `inlier_mask` — which receivers RANSAC accepted
- `n_inliers` — how many of the K receivers agreed

## Install

```bash
pip install numpy
```

No other dependencies. No downloads. No model weights. No
autodiff framework. The whole tool is ~600 lines of numpy.

## Usage

### Programmatic

```python
from ir_source_localizer import localize
import numpy as np

# Load or generate K impulse responses
irs = [ir_0, ir_1, ir_2, ir_3]  # shape (T,), all same sample rate

# Known receiver positions in metres
receivers = [
    (4.5, 3.5, 1.0),
    (0.5, 3.5, 1.0),
    (4.5, 0.5, 1.0),
    (0.5, 0.5, 1.0),
]

result = localize(
    irs=irs,
    sample_rate=48000,
    receivers=receivers,
    sz=1.0,                      # source height (metres)
    x_range=(0.2, 4.8),          # search bounds
    y_range=(0.2, 3.8),
    sigma_guess=4e-4,            # pulse half-width (seconds)
)

print(result)
# source at (+1.503, +1.500) m  ± (3.0, 3.1) cm  inliers 3/4  rms resid 0.352 ms
```

### CLI

```bash
python ir_source_localizer.py localize \
    r0.wav r1.wav r2.wav r3.wav \
    --sample-rate 48000 \
    --receivers 4.5,3.5,1.0 0.5,3.5,1.0 4.5,0.5,1.0 0.5,0.5,1.0 \
    --sz 1.0 \
    --x-range 0.2 4.8 \
    --y-range 0.2 3.8
```

Output:

```
source at (+1.503, +1.500) m  ± (3.0, 3.1) cm  rms arrival residual 0.352 ms  (4 receivers)
```

Pass `--json` for machine-readable output. Pass `--no-unc` to
skip the Monte Carlo uncertainty estimation (2× faster).

### Self-test

```bash
python ir_source_localizer.py self-test
```

11 checks covering pulse detection, RANSAC outlier rejection,
grid search, refinement, end-to-end accuracy, input validation,
and WAV I/O. All pass on a clean install.

### Demo

```bash
python ir_source_localizer.py demo
```

Sweeps noise from 0.5% to 10%, reports position error and
inlier count at each level, then prints a detailed trace for a
single run at 2% noise.

## Benchmarks

Synthetic demo, shoebox room 5×4×2.8 m, four corner receivers,
source at (1.5, 1.5, 1.0), Mexican-hat pulse of 0.4 ms
half-width, 2000 samples at 48 kHz.

| noise | x | y | error | inliers |
|---|---|---|---|---|
| 0.5% | 1.503 | 1.500 | **0.28 cm** | 3/4 |
| 1.0% | 1.503 | 1.500 | **0.28 cm** | 3/4 |
| 2.0% | 1.503 | 1.500 | **0.28 cm** | 3/4 |
| 5.0% | 1.503 | 1.500 | **0.28 cm** | 3/4 |
| 10.0% | 1.507 | 1.495 | **0.89 cm** | 3/4 |

The error is stable across the noise range because RANSAC is
rejecting the one bad receiver. Receiver 2's direct arrival is
systematically biased by ~0.70 ms by an unresolved reflection
that arrives 1.29 ms later at that specific geometry. The other
three receivers detect the direct arrival to within one sample
period (0.02 ms). Once the outlier is dropped, the three remaining
arrivals determine the position to sub-centimetre.

## How it works

1. **Detect direct arrivals.** For each IR, square the signal
   (energy envelope), smooth with a 1-σ window, find the first
   crossing of 30% of the envelope peak, search forward ~1.2 σ
   for the local maximum. Returns a time and a quality.

2. **RANSAC over receiver subsets.** With K ≥ 4 receivers, try
   every 3-subset. Solve the position on the subset, count how
   many of the K receivers agree within an inlier threshold
   (default 0.30 ms). Keep the position with the largest
   consensus.

3. **Refine on inliers.** Take the winning subset's inliers and
   run Adam on the arrival-time residual using only those
   receivers.

4. **Estimate uncertainty.** Monte Carlo: perturb the detected
   arrival times by N(0, 0.3σ), re-localize, return the standard
   deviation of the resulting position distribution.

## The version history is the finding

This tool took six attempts to get right. The earlier versions
failed for specific, instructive reasons:

| version | change | outcome |
|---|---|---|
| v1 | wide-window argmax after rise | locked onto the floor reflection at r2; 35 cm error |
| v2 | first local max after rise | picked up leading-edge bumps; 0.9 ms early bias |
| v3 | half-peak leading edge | smoothing window merged direct with nearby reflection |
| v4 | squared signal + tight smoothing | 12 cm error, r2 still biased 0.70 ms |
| v5 | quality-weighted loss | no improvement — quality ≠ timing accuracy |
| v6 | RANSAC over subsets | **0.28 cm error, inliers 3/4** |

The lesson: **when one of your measurements is unreliable,
reject it — do not try to fix it.** Five iterations of detector
tuning did not rescue receiver 2. RANSAC accepted that receiver
2 is hopeless for this geometry and solved around it.

## When to use it

- **Any application where you have K microphones at known
  positions and a source that emits a short click.** The
  arrival-time problem is well-conditioned and the RANSAC
  wrapper handles one bad receiver.
- **Environments where GPS does not work.** Indoor, underwater,
  underground. Acoustic arrival times give position without
  electromagnetic contact.
- **Cheap hardware setups.** MEMS microphones cost less than a
  dollar each. A four-microphone array with a USB audio
  interface is a few tens of dollars.
- **Situations where the source can emit a known pulse.** Any
  device with a speaker. Any event that produces a click.

## When *not* to use it

- **When the source cannot emit a sharp pulse.** Speech, music,
  and continuous sounds do not have a detectable direct arrival.
  Cross-correlation against the source signal is required, and
  this tool does not implement that.
- **When the room dimensions are unknown.** The RANSAC inlier
  threshold is in arrival-time units, so it works without room
  dimensions, but the demo's synthetic simulator uses shoebox
  geometry. In a real room with unknown layout, the tool still
  works — it does not need the room to be shoebox-shaped, only
  the receiver positions to be known.
- **When you have fewer than 3 receivers.** With 3 receivers,
  the position is exactly determined but there is no redundancy
  for outlier rejection. The tool works, but a single bad
  detection will ruin the result.
- **When the source height is unknown.** The tool assumes `sz`
  is known. Fitting `sz` is a separate problem.

## Honest limitations

- **Tested only on synthetic impulse responses.** Every number
  in the benchmarks section comes from the image-source
  simulator inside the file. Real recordings will have
  backgrounds, unknown source directivity, and non-ideal
  pulses. The tool has not been run on a real recording.
- **Shoebox geometry in the simulator.** The RANSAC positioner
  itself does not assume shoebox — it works on arrival times
  and known receiver positions. But the demo's IRs come from a
  shoebox. Real rooms have different reflection patterns and
  the RANSAC inlier threshold may need tuning.
- **Assumes the source pulse is short and known-width.** The
  `sigma_guess` parameter must approximately match the
  pulse's half-width. If the pulse is very different, detection
  fails.
- **Requires the receiver positions to be known exactly.** A
  centimetre error in receiver position produces a
  centimetre error in the source. Real installations need the
  microphones surveyed or calibrated.
- **Detects the direct arrival, not the source onset.** The
  detected time corresponds to the peak of the envelope, not
  the leading edge. If the source emits a click with a long
  rise time, the detected time is delayed relative to the
  physical emission, and this delay must be calibrated.
- **Only two dimensions.** The source's height is fixed at
  `sz`. Three-dimensional localization would need a fourth
  equation or an independent estimate of `sz`.
- **No beamforming or array processing.** This is not a
  spatial-filtering tool. It localizes one source at a time
  from arrival times.
- **RANSAC over subsets costs O(K³).** With K=4, four subsets.
  With K=10, 120 subsets. With K=20, 1140 subsets. Still fast
  for arrival-time localization (each subset is a 2-D grid
  search), but not for very large arrays.

## What this tool does not include

There are several natural extensions that were considered and
not built:

- **Fitting the room dimensions from the IR.** The
  `room_fitter` experiments in the same session showed that room
  shape is much harder to recover than source position from a
  single IR. Source position is well-conditioned; room shape
  is not.
- **Learning the pulse shape.** The tool requires a
  `sigma_guess`. Auto-estimating the pulse width is a small
  extension but not implemented.
- **Handling moving sources.** The tool assumes the source is
  stationary during the pulse. A moving source needs a
  different formulation (Doppler-aware arrival-time model).
- **Robust to unknown `sz`.** Fixing `sz` is a modelling
  choice; the natural joint estimate of `(sx, sy, sz)` from
  arrival times at four coplanar receivers is
  underdetermined. A receiver array with vertical extent
  would be needed.

## Reference

Part of a series of small tools built in one session:

| tool | reads | answers |
|---|---|---|
| `numerical-provenance` | a numeric pipeline | can I trust this number? |
| `token-provenance` | an LLM pipeline | can I trust this token count? |
| `provenance-reconstruct` | a bare number | what produced this, and how sure are you? |
| `learning-report` | a trained model | can I make an honest claim about what it learned? |
| `sensor-audit` | a set of sensors | which are redundant, which are load-bearing? |
| `room-fitter` | an impulse response | what room produced this? *(failed)* |
| **`ir-source-localizer`** | **a set of IRs** | **where is the source?** |

## License

Apache-2.0