Processing#
sidescanpy.processing corrects, slices and measures frequency-channel
datasets. Every function takes and returns an xr.Dataset (the corrections also
take a whole survey); the input is never modified. Getting started runs the default
correction on a public line, and Correct a line
adds the bottom track.
Corrections#
Each correction is a function. Call them in order on a whole survey (every frequency is corrected and a new survey returned) or on one frequency dataset:
from sidescanpy.processing import (
altitude_from_bottom_track,
apply_tvg,
compute_geometric_ranges,
compute_georeferencing,
interpolate_aux_onto_pings,
)
survey = interpolate_aux_onto_pings(survey) # navigation and attitude onto pings
survey = altitude_from_bottom_track(survey) # only if the file has no altitude
survey = apply_tvg(survey, spreading_loss_db=30) # spreading and absorption
survey = compute_geometric_ranges(survey) # slant and ground range
survey = compute_georeferencing(survey) # latitude and longitude per sample
Function |
Does |
Needs first |
|---|---|---|
|
crops far-range noise past a detected cutoff |
|
|
interpolates the survey’s navigation and attitude records onto every ping ( |
|
|
fills missing altitude from the first bottom return |
|
|
moves ship positions back to the towfish by the recorded layback (only where the file says the positions are the ship’s) |
navigation on the pings |
|
time-varying gain, linear amplitude to dB |
|
|
slant range, ground range, across- and along-track distance per sample |
altitude |
|
subtracts a fitted range curve |
|
|
latitude and longitude per sample |
|
Every call records itself in the dataset’s processing_history with the
parameters it used, and the input is never modified. Leave a call out to
skip that correction, or put your own function in the chain. The
radiometric corrections (apply_tvg, the range correction) refuse to run
twice on the same data, because their gains would stack; to change a TVG
recipe, invert the dataset first. The other functions recompute when
called again.
Running steps by name#
The same corrections can be run from a list of step names, which suits a
config file or the command line (sidescan-correct --steps):
Function |
Applies steps to |
|---|---|
|
one frequency channel |
|
every channel of a survey (feeds the survey’s navigation/attitude records to |
|
a |
from sidescanpy.processing import correct_survey
survey = correct_survey(survey, ["interpolate_aux", "tvg", "geometric", "georef"],
params={"tvg": {"spreading_loss_db": 30.0}})
Use correct_file for files too large for memory, and for adding
several steps to a file: it applies them in one pass, which is much
faster than writing the file after each step.
Built-in steps#
In canonical order. DEFAULT_STEPS is interpolate_aux → tvg → geometric → georef.
Step |
Function |
Parameters |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Steps run in the order given; respecting dependencies is the caller’s job
(georef and range_correction need geometric; bottom_track must run
before geometric to affect it, and layback before georef).
bottom_track and layback are not in DEFAULT_STEPS.
Rules#
These apply to step lists (correct, correct_survey, correct_file).
Skip a step by leaving it out.
paramsmay only name steps that are insteps; a typo raisesValueError, and an unknown parameter raisesTypeError. Both are checked before anything runs.Custom steps. Any callable
fn(ds, **params) -> ds, orStep(name, fn), can be a step. It is recorded in history like a built-in. Give each one a distinct name; lambdas are rejected.Idempotent. A step already recorded in
processing_historyis skipped, so re-running is a no-op.force=Truere-applies it; a radiometric step is inverted first, so new parameters replace the old ones instead of stacking.History. Each applied step appends
{"step", "params", "function", "version", "timestamp_utc"}with the parameters actually used.No raw copy. Corrections modify
backscatter_intensityin place.invert(ds)undoes recorded steps, anduncorrected_backscatter(ds)returns the signal with every radiometric step undone.processing_level(ds)reports the latest applied level (raw,tvg,geometric,range_correction,georef).
TVG#
apply_tvg adds the two-way propagation loss back:
Spreading:
spreading_loss_db * log10(R). The default, 30, models the insonified seafloor patch growing in proportion to range: two-way spherical spreading (40) less 10 log R (e.g. J. Mar. Sci. Eng. 2017, 5(4), 54). Which fits real data varies; measure it per survey withanalysis.fit_tvg_models, which fits no TVG, absorption only and 20/30/40 log R with and without absorption to the raw median level and returns the best asfit.tvg_params, and confirm it withanalysis.check_tvg(Analysis). Set the coefficient in the config. 40 was the default in 0.7.0-0.7.5 and 20 (one-way spreading) before 0.7.0; files corrected with either record the value in their history, soinvert()still undoes them exactly.Absorption:
2 * alpha * R. Whenabsorption_db_per_mis not given, alpha comes from the Francois-Garrison model for the channel’s frequency (francois_garrison_absorption_db_per_m), usingtemperature_c,salinity_psu,depth_mandph(defaults 10 °C, 35 psu, 10 m, pH 8). The resolved value is recorded in history.
Linear amplitude is converted to dB as 20 * log10(clip(x, 1e-6)) first,
so exact zeros map to about -120 dB. A zero or missing sound velocity
falls back to 1500 m/s. Setting a term to 0 turns it off, which is how to
avoid correcting a loss the hardware already compensated.
References:
R. E. Francois and G. R. Garrison, “Sound absorption based on ocean measurements: Part I: Pure water and magnesium sulfate contributions,” J. Acoust. Soc. Am. 72(3), 896-907 (1982).
R. E. Francois and G. R. Garrison, “Sound absorption based on ocean measurements: Part II: Boric acid contribution and equation for total absorption,” J. Acoust. Soc. Am. 72(6), 1879-1890 (1982).
Geometric ranges#
compute_geometric_ranges adds per-sample slant_range_m,
ground_range_m, across_track_m (port negative, starboard positive),
along_track_m (forward positive) and related variables:
Slant range:
sample * sound_vel / (2 * sample_rate), from the absolutesamplecoordinate, so a dataset trimmed of leading samples keeps its true ranges.Ground range (flat bottom):
sqrt(slant² - altitude²)whereslant > altitude, NaN in the water column. A ping with NaN altitude gets NaN ground range (and no georeferenced position); altitude 0 gives ground range equal to slant range. Both are logged at WARNING.Pitch (flat bottom, fan beam perpendicular to the body x-axis, pitch θ positive nose up, altitude h): the fan plane tilts forward by θ and meets the seafloor on a line
h·tan θahead of nadir. Every seafloor sample of the ping getsalong_track_m = h·tan θandacross_track_m = ±sqrt(slant² − (h / cos θ)²); samples withslant <= h / cos θare water column and NaN in both. The horizontal distance from nadir,sqrt(along² + across²), equalsground_range_m, which does not depend on pitch. NaN pitch is taken as 0 and logged at WARNING. Pitch shifts a ping forward or aft by a constant; it does not rotate the swath. Files made by 0.9.0 and earlier usedalong = ground·sin θ(an offset growing with range, about 8.7 m at 100 m and 5°); theirgeometrichistory record lackspitch_model. Roll and yaw are not modelled.
Altitude from the bottom track#
Some recordings have no usable altitude: no altimeter, no bottom lock, or
a recorder that writes 0. altitude_from_bottom_track (the
bottom_track step) measures it from the data: the onset of the bottom
return on each channel (detect_first_bottom_return on the uncorrected
amplitude, refined to the half-rise sample), the nearer channel, in
metres of slant range. By default it fills only pings whose altitude_m
is NaN or <= 0; max_diff_m also replaces recorded values that disagree
with the track by more than that (a lost lock that still writes a value),
and replace="all" replaces every ping.
The return must stay above threshold for min_return_m (default 5 m) of
slant range, of which min_return_fraction (default 70 %) must be above:
a transmit burst lasts a metre or two, a seabed return tens of metres, and
the fraction lets shadows in a rough seabed through. On public test files
with a recorded altimeter (Benthos SIS-1624, about 30 m altitude) the
tracked value agreed to within 0.2 m for 90 % of pings. The first return
comes from the beam’s near edge, so on sloping or very shallow seabed the
tracked value is the slant range to the nearest bottom point, a little
above the true altitude (+0.6 m at 1.7 m altitude on a Klein 3000 file).
Layback#
A towed sonar sits behind the navigation antenna. Readers record what the
per-ping latitude / longitude are the position of in the
position_source attribute, with how they decided in
position_source_reason:
Format |
|
|
otherwise |
|---|---|---|---|
JSF |
validity bit 13 (position interpolated) set: Discover laid it back |
bit 13 clear: the last nav fix, no layback |
|
XTF |
|
never inferred (see below) |
|
SDF |
computed fish position (fields 39/40) set and used |
fell back to the ship position (fields 37/38) |
|
Readers report ship or towfish only where the format documents it
(a validity bit, a field the writer fills only when it laid the fish
back). Anything else is unknown, and the layback step leaves it alone:
whether recorded positions are the ship’s is a fact about the system as
deployed, so it is the user’s call. Where a file looks like ship
positions, the reason says so without acting on it. For example, Triton
Isis XTF with no ShipX/Y and a recorded Layback: on a public Isis
line the georeferenced image lines up with the Isis mosaic only after
the recorded layback is applied, but nothing documents it. Pass
position_source="ship" when you know.
They also carry per-ping layback_m (JSF bytes 228-231, XTF Layback)
and cable_out_m (JSF, in decimetres in the file; XTF CableOut; SDF
field 50), and for JSF the antenna-to-tow-point offset as
tow_point_aft_m / tow_point_starboard_m when it is set.
apply_layback (the layback step) moves each ping astern along its
heading_deg by tow_point_aft_m + layback_m and to starboard by
tow_point_starboard_m, on the WGS84 ellipsoid: a fixed offset astern
from the recorded layback and lever arms, as MB-System applies it. Where
layback_m is NaN it falls back to sqrt(cable_out_m² − depth_m²)
(use_cable_out). It moves positions only when position_source is
ship; for towfish, mixed or unknown it changes nothing and logs a
WARNING, so a layback is never applied twice. A caller who knows the
positions are the ship’s passes position_source="ship". The recorded
positions are kept in latitude_before_layback /
longitude_before_layback, and invert(ds, ["layback"]) restores them
(it also drops georef, which was computed from the moved positions).
apply_layback refuses a dataset that is already georeferenced.
Not modelled: course made good (the heading is used), the fish lagging
the ship’s track in turns (the tractrix model of Swath), tow-point height,
and the navigation aux record (only per-ping positions move; run
interpolate_aux before layback, as the canonical order does).
Georeferencing#
compute_georeferencing adds latitude_per_sample and
longitude_per_sample from the per-ping position, heading and across-track
offset on the WGS84 ellipsoid.
Slicing#
Function |
Keeps |
|---|---|
|
a ping index range |
|
a range of the absolute |
|
samples within a slant-range window |
|
samples within a ground range (needs |
|
a time window |
|
pings (or, with |
|
applies any slicer to every frequency of a survey |
Slicing keeps processing_history.
Bottom, nadir and noise#
Function |
What it does |
|---|---|
|
per-(ping, channel) sample where the bottom echo starts |
|
|
|
NaN everything before the first bottom return |
|
find and zero an electronic noise burst at nadir |
|
rolling median of a per-ping boundary |
|
find and crop far-range noise that rises above the floor |
|
per-range-bin median/MAD outlier rejection |
|
mask pings much brighter than their neighbours |
Segments#
find_segments splits a file into contiguous segments and labels each one
clean or excluded, from gaps in time or position (detect_gaps), hardware
setting changes (detect_setting_changes), dropout or garbage runs
(detect_garbage_runs) and attitude instability (detect_unstable_spans).
split_by_segments returns one sub-survey per clean segment, and
write_segments writes each clean segment to its own <label>.ss.h5.
Profiles and tracks#
backscatter_vs_range/backscatter_vs_sample: per-bin mean, median, std, percentiles and count.along_track_distance_m(ds, fill_nav_stalls=False): cumulative geodesic distance along the track;fill_nav_stallsspreads pings that repeat a navigation fix.smooth_navigation(ds, window_s=5.0): replaces each ping’s position with a straight-line fit over a time window, for recorders whose positions change at every entry but carry an error between GPS fixes (a sawtooth that interpolating between fixes cannot remove). Records asmooth_navigationhistory entry; apply it beforegeoref.course_over_ground(ds, half_window_s=4.0): per-ping course from the positions either side, to use asheading_degwhen no compass is recorded or trusted.build_survey_track(surveys, freq_key, scanline_stride=): nadir track and strided scan-line endpoints as aSurveyTrack.