Skip to content

Tier-2 backscatter correction: remove per-beam 2-way TL (depth-transferable angular response) (#87)#88

Merged
rolker merged 7 commits into
jazzyfrom
feature/issue-87
Jun 29, 2026
Merged

Tier-2 backscatter correction: remove per-beam 2-way TL (depth-transferable angular response) (#87)#88
rolker merged 7 commits into
jazzyfrom
feature/issue-87

Conversation

@rolker

@rolker rolker commented Jun 29, 2026

Copy link
Copy Markdown
Owner

Summary

"Tier-2" backscatter correction: remove the per-beam 2-way transmission loss
(spreading + freshwater absorption) so the empirical angular-response curve
(cube#81) becomes depth/range transferable, not locked to one survey's depth.

corrected = raw + (40·log₁₀R + 2·α·R) − residualCurve(|beam_angle|),
R = twtt·c/2 per beam, α = freshwater Francois-Garrison absorption.

The TL is added back (compensated): a distant return lost more energy, so it's
boosted to recover range-independent backscatter (TVG-style).

Design

  • Self-describing curve. The CSV header carries tl_removed: true +
    absorption_db_per_m; the estimator applies TL iff the curve says so, so
    tier-1 curves keep working under the same --backscatter-correction empirical.
    The Francois-Garrison absorption lives only in the Python derive tool (single
    source of truth); the C++ estimator reads the scalar α from the header.
  • Per-beam slant range threaded like beam_angle (offline: twtt·c/2; live:
    sensor-frame norm, exact for the M3's untilted array, bounded otherwise).
  • derive_angular_response.py gains multi-bag support + --remove-tl.

Validation (real M3 data, bizzyboat June bags)

  • The sign matters: subtracting TL (an earlier draft) made the curve steeper +
    the cross-bag spread larger; adding it back flattens the curve (June 12
    edge: −16.7 → −7.0 dB) and the corrected backscatter store is range-flattened.
  • Built bathy (Processed) + backscatter (Processed, corrected) stores end-to-end
    from a real bag; the backscatter mosaic looks range-compensated.
  • colcon build + colcon test: 404 tests, 0 failures; lint clean.
  • Deep pre-push review (2 adversarial lenses), approved.

Out of scope (follow-ups)

Closes #87


Authored-By: Claude Code Agent
Model: Claude Opus 4.8 (1M context)

Claude Code Agent and others added 7 commits June 28, 2026 14:02
)

Make the empirical angular-response correction range/depth transferable by
removing the per-beam 2-way transmission loss before subtracting the residual
curve:  corrected = raw - (40*log10(R) + 2*alpha*R) - residualCurve(|angle|).

The curve file is self-describing: one Empirical mode, TL applied iff the loaded
curve header says tl_removed=true. Tier-1 curves keep working unchanged. The C++
estimator reads the scalar absorption_db_per_m from the header verbatim and never
recomputes alpha, so Python/C++ apply an identical TL.

Per-beam slant range R is threaded as a new sufficient statistic exactly like
beam_angle: Sounding::slant_range -> DepthAndUncertainty::range (pack(1) raster
struct grows to 20 bytes; layout-safe, every raster read strides by
sizeof(DepthAndUncertainty)) -> Node::queueEstimate/update -> recordBeam ->
BeamIntensitySample::range. The offline importer threads R from the Sounding
detections ctor; the live node recovers R as the sensor-frame point norm. NaN /
non-positive R skips the TL term. Loader parses the tl_removed/absorption_db_per_m
header comments (absent -> tier-1 defaults).

11 new C++ tests (tier-2 TL+residual, nadir TL-only, NaN/non-positive-R skip,
tier-1 ignores range, range threads through update; loader header parse + tier-1
defaults + back-compat loader). 404 tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…t of #87)

- Accept multiple bags (nargs='+') and merge per-bin sums; a real survey
  calibration spans many bags.
- --remove-tl: subtract per-beam 2-way TL 40*log10(R)+2*alpha*R before binning,
  so the curve becomes a depth/range-transferable residual. R = twtt*c/2 per beam
  (beams with non-positive/NaN R skipped). Single pass: the spreading term is
  removed inline, the absorption term once alpha is known from the median
  ping_info.frequency.
- alpha = Francois-Garrison freshwater pure-water absorption from --water-temp-c
  (required with --remove-tl) and the per-ping frequency; sanity 500kHz/24C ->
  0.0490 dB/m. Fresh water only: --salinity>0 (seawater) rejected as out of scope.
- Writes self-describing header (tl_removed / absorption_db_per_m / water_temp_c /
  tl_model) the C++ estimator reads verbatim, guaranteeing an identical TL.

py_compile clean; alpha value + synthetic TL round-trip verified.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Document the tier-2 transition: the empirical curve can now be TL-removed and
depth/range transferable; self-describing header + alpha-in-Python single-source
contract; per-beam R threading; fresh-water-only scope. Note the TVG/absorption/
frequency state ultimately belongs in SonarInfo (unh_marine_autonomy#240) and the
full GeoCoder (area+beam-pattern) is the deferred tier-3.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The 2-way transmission loss must be ADDED BACK to remove it: a distant return
lost more energy, so it is boosted to recover range-independent backscatter
(TVG-style). The initial implementation subtracted it, which made far beams
even dimmer -> steeper angular curve + larger cross-bag spread (the wrong
direction). Validated on the June bizzy data: the fixed sign FLATTENS the
curve (edge -16.7 -> -7.0 dB on June 12) and the corrected backscatter store
looks range-compensated.

- node.cpp extractNodeRecord: corrected = raw + TL(R) - residualCurve.
- derive_angular_response.py: add back 40*log10(R) (inline) + 2*alpha*R (write_csv).
- test_node.cpp: tier2Expected reference flipped to raw + tl - residual.
- ADR-0007 addendum: corrected sign + a sign note.

404 tests green.

Part of #87
- hypothesis.h: stale inverted-sign formula in the range-field doc -> raw + TL.
- cube_bathymetry_node.cpp: the live-path slant_range = position norm equals
  twtt*c/2 EXACTLY only when tx_angle==0 (true for the M3's flat downward array,
  tx=0); documented the bounded R*sqrt(1+sin^2(tx)sin^2(rx)) case for a future
  tilted-tx sonar. Offline import uses twtt*c/2, so live/offline match for the M3.
- derive tool: guard freqs accumulation on --remove-tl; note the intentional
  drop-vs-retain asymmetry for no-range beams; parse_args docstring.

Part of #87
Copilot AI review requested due to automatic review settings June 29, 2026 03:59

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@rolker
rolker merged commit 66f2625 into jazzy Jun 29, 2026
1 of 2 checks passed
@rolker
rolker deleted the feature/issue-87 branch June 29, 2026 04:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Tier-2 backscatter correction: remove per-beam 2-way TL (range/absorption) for a depth-transferable angular response

2 participants