Use when you must locate a stationary emitter from passive angle-of-arrival bearing lines measured at known observer positions. Build the linearized bearing equations sin(beta_i)*(x - xi) - cos(beta_i)*(y - yi) = 0 per observer, solve the Stansfield closed-form weighted least squares for the two-dimensional emitter fix with an equal-angle first pass and a distance-weighted second pass, and read the fix covariance error ellipse plus the per-bearing residual angles. Produces the emitter positio...
Scanned 9/27/2026
Install to Claude Code
npx -y skills add ashfordeOU/aero-agent-skills --skill bearing-only-localization --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Bearing Only Localization?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/ashfordeou-bearing-only-localization)More formats (shields.io, HTML) on the badges page.
---
name: bearing-only-localization
description: "Use when you must locate a stationary emitter from passive angle-of-arrival bearing lines measured at known observer positions. Build the linearized bearing equations sin(beta_i)*(x - xi) - cos(beta_i)*(y - yi) = 0 per observer, solve the Stansfield closed-form weighted least squares for the two-dimensional emitter fix with an equal-angle first pass and a distance-weighted second pass, and read the fix covariance error ellipse plus the per-bearing residual angles. Produces the emitter position fix, the 1-sigma error ellipse, the per-bearing residuals and the observer-geometry dilution verdict. Trigger: bearing-only-localization, angle-of-arrival-triangulation, stansfield-weighted-least-squares, bearing-line-intersection, emitter-fix-covariance, per-bearing-residual, observer-geometry-dilution."
license: Apache-2.0
compliance: STANDARDS-REF
standards:
- id: arp4754a
reference-only: true
gated: false
domain: gnc-autonomy
pack: navigation
compatibility: "agentskills.io SKILL.md; any SKILL host (Claude Code, Hermes, OpenClaw)"
metadata:
domain: gnc-autonomy
subdomain: navigation
tags: [bearing-only-localization, angle-of-arrival-triangulation, stansfield-weighted-least-squares, bearing-line-intersection, emitter-fix-covariance, per-bearing-residual, observer-geometry-dilution]
version: 0.1.0
author: Aero Agent Skills
---
# Bearing-Only Localization (gnc-autonomy/navigation/bearing-only-localization)
Use when you must locate a stationary emitter from passive
angle-of-arrival bearing lines measured at known observer positions:
each observer measures the direction to the emitter, never a range, and
the batch of N bearing lines is fused into one static two-dimensional
fix. This leaf implements the classical Stansfield closed-form weighted
least squares in pure Python, stdlib only, with no RNG and no imports
beyond math. It pairs with gnc-autonomy/navigation/dilution-of-precision
for geometry quality reads on navigation problems and with the
kalman-filter-design and estimation-filtering leaves for the recursive
tracking problems this leaf deliberately does not touch. The emitter is
stationary, the observers sit at known fixed positions and all bearings
belong to a single epoch: batch fix only, no time recursion.
## Domain quick reference
- Bearing line: observer i at o_i = (xi, yi) in m, measured bearing
beta_i in degrees from the +x axis toward +y, in [0, 360), pointing at
the emitter z = (x, y). The line equation is
sin(beta_i)*(x - xi) - cos(beta_i)*(y - yi) = 0, written with the unit
line normal g_i = (sin beta_i, -cos beta_i) as g_i . z = g_i . o_i.
Stacking N rows gives A z = b; A is N x 2 and every row is a unit
vector, so the normals carry no range information.
- Stansfield pass 1, equal-angle weights: sigma_i is the 1-sigma bearing
error in degrees, converted to radians inside as sigma_i * pi/180.
With W_ii = 1/sigma_i^2 the first estimate is the exact closed form
z1 = (A^T W A)^-1 A^T W b with the 2x2 inverse of the symmetric normal
matrix.
- Stansfield pass 2, range weights: r_i = |z1 - o_i| and
W_ii = 1/(sigma_i^2 * r_i^2). An angular error sigma_i on a line at
range r_i displaces the line cross-range by about sigma_i * r_i, so
the position-domain weight of the line falls as 1/r_i^2 and distant
lines are down-weighted. The final fix is z2.
- Fix covariance: C = (A^T W A)^-1 evaluated with the pass-2 weights, a
symmetric 2x2 matrix in m^2.
- 1-sigma error ellipse: eigenvalues lambda_max >= lambda_min > 0 of C
give the semi-axes a = sqrt(lambda_max) and b = sqrt(lambda_min) in m;
the major-axis orientation is 0.5*atan2(2*C01, C00 - C11) wrapped into
[-180, 180) degrees.
- Per-bearing residual angle: the bearing predicted at the fix is
beta_hat_i = atan2(y - yi, x - xi) in [0, 360); the residual is
delta_i = wrap180(beta_i - beta_hat_i), the signed angular leftover of
line i after the fix, and the residual RMS is the RMS of those wrapped
values.
- Observer-geometry dilution: with S = sum_i g_i g_i^T the Gram matrix
of the unit normals, d = 1/sqrt(lambda_min(S)), dimensionless. d = 1
exactly for any orthogonal pair of lines; three lines spread around
the emitter give d < 1 because the redundancy beats the orthogonal
two-line baseline; near-parallel lines push lambda_min(S) toward 0 and
d large. Verdicts: d <= 1.05 good observer spread, d < 2.5 moderate,
d >= 2.5 poor (near-parallel bearing lines).
- Three-dimensional refinement: observer i at (xi, yi, zi) measures
azimuth (degrees from +x toward +y) and elevation (degrees above the
horizontal); the unit line of sight is u_i = (cos el * cos az, cos el
* sin az, sin el) and the residual e_i = u_i x (z - o_i) is driven to
zero by the damped Gauss-Newton step (J^T J + mu I) dz = -J^T e with
mu = 1e-12 * trace(J^T J)/3.
- Units: distances in m, angles in degrees at the interface, radians
inside the module.
## Workflow
1. Assemble the bearing-line measurement set: the observer positions
(xi, yi), the measured bearing beta_i from each observer to the
emitter in degrees (bearing_deg computes it toward a known target)
and the per-line 1-sigma bearing error sigma_i in degrees. At least
two observers with distinct bearing lines are required.
2. Form the linearized bearing equations and solve the equal-angle
first pass: per observer sin(beta_i)*(x - xi) - cos(beta_i)*(y - yi)
= 0, stacked as A z = b, weighted with W_ii = 1/sigma_i^2 and solved
by wls_fix pass 1, giving the first estimate z1 (pass1_x_m,
pass1_y_m). A singular normal matrix (parallel or coincident bearing
lines) raises ValueError here.
3. Run the Stansfield distance-weighted second pass: pass-1 distances
r_i = |z1 - o_i|, weights W_ii = 1/(sigma_i^2 * r_i^2), final fix z2
(x_m, y_m) and the fix covariance C = (A^T W A)^-1 in m^2, all
returned by the same wls_fix call with sigma_deg given (ranges_m,
iterations 2). Without sigma_deg the solver returns the
geometry-only unweighted fix with iterations 1.
4. Read the 1-sigma error ellipse of the fix covariance with
error_ellipse: semi-major a, semi-minor b and the major-axis
orientation in degrees, wrapped to [-180, 180).
5. Compute the per-bearing residual angles, measured minus the bearing
predicted at the fix and wrapped to [-180, 180) degrees, with
residual_angles_deg, and their RMS with residual_rms_deg. Residuals
near zero with an RMS well below sigma mean the line set is
consistent with the fix.
6. Assess the observer geometry with geometry_dilution_factor and read
the observer-geometry dilution verdict from dilution_verdict (good
observer spread, moderate, or poor near-parallel lines). Reject or
flag the fix when the verdict is poor.
7. Optionally refine the fix in three dimensions with
refine_3d_gauss_newton when azimuth and elevation angles are
available at each of at least three observers: start from the 2-D fix
lifted to z = 0 and iterate the damped normal step to the 3-D
emitter position.
8. Close out with the deterministic contract test
scripts/test_bearing_only_localization.py, which pins every worked
output above and the ValueError rejections.
## Worked example
Three observers on a 1 km right triangle at (0, 0), (1000, 0) and
(0, 1000) m localize an emitter near (300, 400) m. The measured
bearings are the exact observer-to-emitter bearings corrupted by fixed
deterministic offsets (+0.5, -0.3, +0.2 deg), sigma 0.5 deg per line.
Module outputs (stdlib math, closed form, deterministic):
- Geometry: observer-to-emitter ranges 500.000, 806.226 and 670.820 m;
true bearings 53.130102, 150.255119 and 296.565051 deg; measured
bearings 53.630102, 149.955119 and 296.765051 deg.
- Pass-1 fix (equal-angle weights 1/sigma^2): (299.178622, 405.821689)
m, error 5.879347 m against the truth, with pass-1 ranges to the fix
504.182, 809.841 and 665.249 m.
- Final Stansfield fix (range weights 1/(sigma^2 * r^2)):
(299.150033, 405.988514) m, error 6.048532 m, 2 passes: the range
re-weighting moves the fix by less than 0.2 m on this spread
geometry.
- Fix covariance C = [[16.195587, 1.677004], [1.677004, 25.693501]]
m^2; the 1-sigma error ellipse has semi-major 5.097147 m, semi-minor
3.988506 m and orientation 80.275145 deg, consistent with the about
6 m fix error.
- Per-bearing residual angles at the final fix (measured minus the
bearing predicted at the fix): +0.014509, +0.037982 and +0.034776
deg, residual RMS 0.030890 deg: after the fix every line keeps a
small angular leftover, well below the 0.5 deg sigma.
- Observer-geometry dilution factor d = 0.957062, verdict good observer
spread: the three normals spread around the emitter condition the
line set better than an orthogonal pair (d = 1).
- Weighting sanity: raising sigma of line 1 to 10 deg with the others
at 0.5 pulls the fix to (300.194425, 404.784548) m, error 4.788497
m, toward the two-line intersection of the remaining lines.
- Optional 3-D Gauss-Newton refinement (bounded, exact azimuth and
elevation): observers at z = 0, emitter (300, 400, 50) m, start
(280, 380, 60) m refines to (300.000000, 400.000000, 50.000000) m in
2 iterations with residual norm 1.2e-13.
## Verification
- Confirm wls_fix on the noisy worked set returns pass-1
(299.178622, 405.821689) and final (299.150033, 405.988514) within
1e-6 m, with both fix errors inside the 0-50 m expectation for
sub-degree noise on 1 km baselines.
- Confirm the covariance entries 16.195587, 1.677004 and 25.693501
m^2 and the ellipse semi-axes 5.097147 m, 3.988506 m at orientation
80.275145 deg.
- Confirm the per-bearing residuals +0.014509, +0.037982 and +0.034776
deg with residual RMS 0.030890 deg, and the dilution factor 0.957062
with the good observer spread verdict.
- Confirm the closed-form identities: noiseless exactness (fix returns
the emitter, residuals zero), trace(C) = a^2 + b^2 and
det(C) = a^2 * b^2 within 1e-12 relative, sigma x 2 scaling both
semi-axes by 2, pass invariance on equal observer ranges, the
weighting pull of a high-sigma line, d = 1 for an orthogonal pair and
d < 1 for the three-line set.
- Confirm ValueError rejection of non-physical inputs: fewer than two
observers, bearing or sigma count mismatches, non-finite coordinates,
bearings or sigma, sigma <= 0, a singular normal matrix (parallel or
coincident bearing lines), an observer within 1e-9 m of the pass-1
fix, an indefinite covariance in error_ellipse, a singular Gram
matrix in geometry_dilution_factor, and in refine_3d_gauss_newton
fewer than three observers, an elevation outside [-90, 90],
max_iters below 1 and non-convergence within max_iters.
- Run the contract test offline: python3
scripts/test_bearing_only_localization.py (35 tests, deterministic).
## Related leaves
- gnc-autonomy/navigation/gnss-pseudorange-positioning: receiver fixes
from satellite ranges with a clock offset, the range-based neighbor
of this angle-only fix.
- gnc-autonomy/navigation/dilution-of-precision: geometry quality reads
(DOP values) for navigation measurement sets.
- gnc-autonomy/navigation/navigation-frames: ECEF/NED/WGS-84 frame
conventions for observer and emitter coordinates.
- gnc-autonomy/estimation-filtering/extended-kalman-filter: recursive
state estimation of a moving target from sequential range and bearing
measurements over time, the tracking alternative to this batch
angle-only fix.
- gnc-autonomy/guidance/collision-course-guidance: the constant-bearing
closing triangle of pursuit guidance, which is a velocity-pointing
geometry and not a bearing-line localization operation.
## Pitfalls
- Treating the dilution factor as an accuracy number: d describes
observer geometry only; the fix accuracy also scales with sigma and
range through the covariance C = (A^T W A)^-1. Read d for the verdict
and C for the error ellipse.
- Fusing nearly parallel bearing lines: near-parallel lines push the
normal Gram matrix toward singular, d >= 2.5 (poor spread) and the
fix covariance blows up along the weak direction, so the ellipse
semi-major axis flags the unobservable axis long before the fix looks
wrong.
- Feeding rounded bearings: the measured bearings enter as the fixed
values at full precision; rounding them to a few decimals before the
solve shifts the fix by about range times the rounding angle, which
can exceed the covariance ellipse size on 1 km baselines.
- Expecting the range-weighted second pass to move the fix a lot: on
well-spread geometry with comparable observer ranges the Stansfield
re-weighting moves the fix by less than 0.2 m (worked example), while
on geometry with one distant line it is exactly the correction that
matters.
- Extending this leaf to moving emitters: the fix is batch-only for a
stationary emitter at a single epoch; recursive bearing tracking over
time belongs to the estimation-filtering leaves.
- Reading the ellipse as a hard guarantee: the covariance is the
first-order linearized spread of the fix, consistent with the
residual RMS but not a statement about systematic bias.
## Behavior contract (gate 3)
Run the deterministic contract test (stdlib unittest, offline):
python3 scripts/test_bearing_only_localization.py
The test covers bearing_deg and the wrap contract, the worked noisy set
through both Stansfield passes (fix, fix error, pass-1 ranges,
covariance entries), the 1-sigma error ellipse with the trace and
determinant identities, sigma scaling, pass invariance on equal ranges,
the per-bearing residual angles and RMS, the weighting pull of a
high-sigma line, the dilution factor and verdict thresholds, noiseless
exactness, the 3-D Gauss-Newton refinement worked case, the ValueError
rejections across the module, and run-to-run determinism. All numeric
asserts are order-safe tolerances (assertAlmostEqual delta or
math.isclose), never exact float equality on computed sums.
## Compliance
- Standards referenced, not reproduced: ARP4754A (Guidelines for
Development of Civil Aircraft and Systems) frames the system
development context; the bearing-line and Stansfield relations above
are standard engineering methodology, summary-only per
standards-map.yaml.
- Revision note: ARP4754B (2023) supersedes ARP4754A; this skill keys to
ARP4754A as the certification-baseline revision (FAA AC 20-174 cites A);
see standards-map.yaml arp4754a.revision_decision.
- compliance: STANDARDS-REF, gated: false.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!