Tier-2 backscatter correction: remove per-beam 2-way TL (depth-transferable angular response) (#87)#88
Merged
Merged
Conversation
) 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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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/2per 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
tl_removed: true+absorption_db_per_m; the estimator applies TL iff the curve says so, sotier-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.
beam_angle(offline:twtt·c/2; live:sensor-frame norm, exact for the M3's untilted array, bounded otherwise).
derive_angular_response.pygains multi-bag support +--remove-tl.Validation (real M3 data, bizzyboat June bags)
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.
from a real bag; the backscatter mosaic looks range-compensated.
colcon build+colcon test: 404 tests, 0 failures; lint clean.Out of scope (follow-ups)
unh_marine_autonomy#240), beam-pattern, source-level, slope-aware
incidence (Slope correction is disabled (commented out) in Node::insert #15/Port the CUBE predicted-surface producer (external-prior load + per-sounding touchdown interpolation) to make slope correction active #59).
seeding CUBE with the contour
Chartprior to activate blunder rejection).Closes #87
Authored-By:
Claude Code AgentModel:
Claude Opus 4.8 (1M context)