sidescanpy.processing.range_correction#

Empirical range correction for the residual TVG range-dependence.

processing/radiometric.py’s apply_tvg() implements the physically- grounded spreading+absorption TVG model. Even after that model is applied real data can show a remaining, altitude-invariant, non-monotonic range-dependent residual. This module does not attempt to model the physical cause (it is not a mistuned absorption coefficient or a missing grazing-angle/beam-pattern correction). Instead it fits and removes whatever range-dependent shape is actually observed in TVG-corrected data, per frequency.

The library ships no calibration table: a table is fit from a specific survey and instrument, so it belongs with the project that fitted it. Fit one with fit_range_correction(), store it with RangeCorrectionTable.save(), and pass it (or its path) as the table argument of apply_empirical_range_correction() / the "range_correction" step.

This is a separate, optional correction step ("range_correction") from apply_tvg() — not in sidescanpy.processing.DEFAULT_STEPS; opt in by listing it in the steps passed to sidescanpy.processing.correct.

2026 rshomberg@uri.edu

Contents

STEP_NAME

str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str

RangeCorrectionTable

Per-frequency empirical range-correction lookup.

fit_range_correction

Fit an empirical range-correction curve from one TVG-corrected channel.

apply_empirical_range_correction

Apply a previously-fit empirical range correction to TVG-corrected backscatter.

range_correction_db

Correction (dB) of a piecewise-linear curve evaluated at ds["ground_range_m"].

sidescanpy.processing.range_correction.STEP_NAME = 'range_correction'#

str(object=’’) -> str str(bytes_or_buffer[, encoding[, errors]]) -> str

Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.__str__() (if defined) or repr(object). encoding defaults to sys.getdefaultencoding(). errors defaults to ‘strict’.

class sidescanpy.processing.range_correction.RangeCorrectionTable#

Bases: object

Per-frequency empirical range-correction lookup.

Each calibrated frequency stores a piecewise-linear correction curve (range-bin centers -> correction_db) fit by fit_range_correction(). This is the “non-parametric smooth interpolation over the observed bins” approach from the design doc — deliberately not a fitted polynomial, since the two frequencies calibrated so far have qualitatively different residual shapes (a smooth bowl vs. a sharp near-field rise-then-plateau) that a low-order polynomial risks fitting poorly or overfitting.

add_curve(freq_hz, range_m, correction_db, fit_metadata=None)#

Add or replace the calibrated curve for one frequency.

matched_frequency(freq_hz, tolerance_hz=1000.0)#

The calibrated frequency key matching freq_hz, or None.

correction_db_at(freq_hz, range_m, tolerance_hz=1000.0)#

Evaluate the fitted correction curve for freq_hz at range_m.

Piecewise-linear interpolation between calibrated range-bin centers. range_m values outside the calibration data’s observed span are clamped to the nearest edge’s correction value (np.interp’s default constant-extrapolation behavior) — a deliberate, documented policy, not an accident: the calibration data doesn’t cover ranges never seen in the fitting survey, and clamping is the safest default versus extrapolating an unverified shape.

Returns:
np.ndarray, same shape as range_m, or None if no calibrated
curve matches freq_hz within tolerance_hz — caller decides
what to do (see apply_empirical_range_correction, which skips
the correction with a warning rather than guessing).
save(path)#

Write the table to a JSON file (see to_dict).

classmethod load(path)#

Read a table written by save.

sidescanpy.processing.range_correction.fit_range_correction(ds, bin_width_m=5.0, exclude_below_db=-110.0)#

Fit an empirical range-correction curve from one TVG-corrected channel.

Parameters:
dsxr.Dataset

Must be TVG-corrected and have a ground_range_m variable — i.e. already run through the "tvg" and "geometric" steps.

bin_width_mfloat, default 5.0

Passed through to processing.profiles.backscatter_vs_range.

exclude_below_dbfloat, optional, default -110.0

Passed through as exclude_below_db to exclude the floor-clipped samples (~-120 dB exact-zero-amplitude floor) from biasing the fit. None disables filtering.

Returns:
(range_m, correction_db, fit_metadata)

range_m/correction_db are bin-center arrays ready for RangeCorrectionTable.add_curve(). correction_db[i] is defined as target_level_db - mean_backscatter[i], where target_level_db is the count-weighted mean across all bins — i.e. the fitted correction has ~zero net effect on the survey’s average absolute level, it only flattens the range-dependent shape.

Raises:
ValueError

If ds isn’t TVG+geometric corrected, or no samples survive filtering.

sidescanpy.processing.range_correction.apply_empirical_range_correction(ds, table=None)#

Apply a previously-fit empirical range correction to TVG-corrected backscatter.

Parameters:
dsxr.Dataset or SideScanSurvey

TVG-corrected ("tvg" in processing_history) with a ground_range_m variable — i.e. already run through the "tvg" and "geometric" steps. A SideScanSurvey is corrected frequency by frequency with the same arguments; a new survey is returned and the input is not modified.

tableRangeCorrectionTable or path

Required. The calibration to apply, or the path of a JSON file written by RangeCorrectionTable.save(). The library has no built-in default table.

Returns:
xr.Dataset or SideScanSurvey

Same type as ds: a new dataset with backscatter_intensity shifted by the fitted curve and a "range_correction" history record holding the curve actually applied (so it can be inverted without the table).

Samples with non-finite ground_range_m (masked water-column samples) receive a zero correction rather than propagating NaN into previously-finite backscatter values. Floor-clipped samples (exactly FLOOR_DB) are left at the floor, as by apply_tvg().

If no calibrated curve matches this dataset’s freq_hz_start, logs a warning and returns ds unchanged (no history record) — never guesses at an uncalibrated frequency.

Raises:
TypeError

If table is missing or not a table / path.

ValueError

If ds isn’t TVG+geometric corrected, or the range correction is already recorded in processing_history (refuses to double-apply).

sidescanpy.processing.range_correction.range_correction_db(ds, range_m, correction_db)#

Correction (dB) of a piecewise-linear curve evaluated at ds["ground_range_m"].

Clamped outside the curve’s span; non-finite ground range gets 0 dB. Shared by the forward step and its inversion.