FhSim  3.1.0
Marine systems simulation
Loading...
Searching...
No Matches
0040 — The echosounder upward beam bins one range bin above the sea surface
ID 0040
Class BUG
Severity 1
Status ready
Models Auv/Echosounder
Found 2026-09-22

Evidence

src/auv/SensorEchosounder.cpp, SensorEchosounder::WriteBeam:

const int numBins = std::min(BinIndexOf(beamRange, m_settings.binSize) + 1, kMaxBinsPerBeam);

beamRange for the upward beam is RangeToSurface(...), the range at which the beam crosses z = 0. Taking floor(range / binSize) + 1 bins means the last bin straddles the surface, and the bin is evaluated at its centre:

const double range = (bin + 0.5) * m_settings.binSize;
const double sample[3] = {..., position[2] + direction[2] * range};

With the transducer at 40 m and BinSize = 1, the upward beam has 41 bins and bin 40 is evaluated at 40.5 m, that is at z = -0.5 m, half a metre above the mean water level. environment::field::DepthLayeredScalarField::GetValue clamps outside its profile and returns the shallowest layer's value together with OUT_OF_RANGE_INACCURATE, and this class ignores the status, so the bin reports the surface concentration as though the air above the sea were full of Calanus. tests/in/Auv/Auv_EchoGram_in.xml shows it directly: all 41 bins of every upward row come back at the same Sv.

The same argument applies to the downward beam's bottom bin, which straddles the seabed and is evaluated below it. That one is much less visible, because the bottom bin is dominated by the bottom echo, and because a species field's value below the seabed is usually the same as just above it.

Effect

One bin per upward row, out of typically forty, carries backscatter from a point that is not in the water. At the default 1 m bin size it is at most half a metre of the beam, so the practical consequence for an integrated water-column biomass is well under 2 % and in the direction of a small overestimate. It matters more as BinSize grows: at BinSize = 5 the last bin is evaluated up to 2.5 m into the air.

It also produces a row whose last bin is not really a measurement, which a downstream consumer integrating "all bins" has no way to know.

Possible fix

Two options, and they answer different questions:

  1. Drop the straddling bin: use ceil(range / binSize) rather than floor(...) + 1, so the beam is binned only over whole bins fully inside the water. Simple, and it makes NumBins mean "bins that are entirely measurements". It loses the partial bin, which at the surface is where a bubble layer would be.
  2. Keep the bin but clamp the evaluation point to the water column, sampling at min((k + 0.5) * BinSize, rangeToSurface) for the last bin only, and record in the file header that the last bin of a beam is partial. That keeps the coverage and fixes the physics.

Option 2 is the recommendation: the partial bin is the one a surface-echo model would later want, and clamping is two lines.

Whichever is taken, GetScalarValue's marenv::Status should stop being discarded. A returned OUT_OF_RANGE_INACCURATE is exactly the signal that a bin was evaluated outside the field's domain, and it is currently thrown away at SensorEchosounder::WriteBeam's call site.

Test that would prove it

Extend Auv_EchoGram with a species field whose profile is zero above 5 m depth and constant below it. The present code reports the same non-zero Sv in the last upward bin as in the rest of the beam; either fix makes that bin either absent (option 1) or equal to the value at the surface itself (option 2). A second case at BinSize = 5 makes the error large enough to see without a contrived profile.

Risk

Option 1 changes NumBins on every beam whose range is not an exact multiple of the bin size, so any consumer of an existing echogram file has to be rerun. Both options change one bin of every upward row, so any recorded echogram becomes stale. No baseline currently records one — tests/AuvEchosounder_Test.cpp asserts in closed form — so the cost today is only the two assertions in Auv_EchoGram that name 41 bins.