sidescanpy.io.xtf.converter#
Convert XTF sonar pings into a SideScanSurvey.
Takes the per-file output of sidescanpy.io.xtf.read_xtf() (one
XtfPingData per file) and builds a
validated survey shaped like the JSF and SDF ones:
One frequency channel per port/starboard pair sharing a
ChanInfofrequency, keyedhz_<Hz>.ChanInfo.Frequencyis in kHz (the per-channel header’sFrequencyis a 16-bit kHz value too); a value of 10 000 or more cannot be a sidescan frequency in kHz and is taken as Hz (logged at DEBUG).backscatter_intensityis(ping, channel, sample)float32 with channel 0 = port, 1 = starboard. Samples beyond a ping’sNumSamplesand a side absent from a ping are NaN;valid_samples(ping, channel) holds the recorded count (0 where absent), as in the SDF converter. Sample 0 is nearest the sonar on both sides: XTF stores port samples far -> near (so[port | stbd]reads as one across-track line), and the converter reverses each port ping’s valid samples. Verified on real EdgeTech 4200 files: the first bottom return of the reversed port data tracksSensorPrimaryAltitudethe way the starboard data does as stored.Samples are multiplied by
2**-Weightfrom each channel header. EdgeTech Discover writes the raw block-floating-point samples and puts the JSF weighting factor N inWeight, which can change from ping to ping; scaling makes the samples equal to the JSF ones for the same ping (checked on a public Discover 7.22 recording with both files). Writers that leaveWeightat 0 are unaffected. The range seen is recorded inxtf_weight_min/xtf_weight_max. Some older Klein-written files hold garbage (e.g. -8408); blocks with|Weight|above 30 are left unscaled, with a WARNING, and counted inxtf_weight_ignored.Each frequency has its own ping axis. A ping holding neither channel of a frequency (some files’ early packets carry only the low-frequency pair, see
sidescanpy.io.xtf.reader) is left out of that frequency, logged at DEBUG and counted in thextf_pings_absentattribute.Per-ping fields come from the ping header:
SensorYcoordinate/SensorXcoordinate→latitude/longitude(degrees withNavUnits3; withNavUnits0 they are northing / easting in metres and are converted, see below),SensorHeading,SensorPitch(positive nose up),SensorRoll(positive roll to starboard, i.e. port up, the JSF convention),Heave,SensorDepth→heading_deg,pitch_deg,roll_deg,heave_m,depth_m.SensorPrimaryAltitude→altitude_mandfathometer_reading_m, with 0 (no bottom lock) as NaN, the way the JSF converter turns an invalid altitude into NaN.Times from
Year…Second+HSeconds(10 ms resolution, the recorder’s time base, normally UTC).
Sound velocity and sample rate#
Some recorders write half the sound velocity in SoundVelocity (the
one-way factor c / 2). A value below 1000 m/s is not a plausible
sound speed in water, so it is doubled (logged at DEBUG); a plausible
value is kept. Where the channel header gives both SlantRange and
TimeDuration the choice is cross-checked against SlantRange /
TimeDuration (which is c / 2) and a disagreement over 5 % logged at
WARNING. Zero or negative values become NaN, so downstream code falls
back to 1500 m/s as for SDF.
sample_rate_hz is NumSamples / TimeDuration (falling back to
NumSamples * c / (2 * SlantRange) when TimeDuration is 0), so
the slant range of the last sample, n * c / (2 * fs), equals
TimeDuration * c / 2. The survey model has one sample rate per
frequency. Files whose range setting changes mid-file (the sample count
staying fixed) are meant to be split first with
sidescanpy.io.xtf.split_by_sample_rate() and each run converted
on its own. When unsplit pings still mix rates, the most common rate is
used and the pings at other rates are reported at WARNING and counted in
xtf_pings_other_sample_rate; their ranges are then wrong by the ratio
of the rates. For split runs the tolerance is twice the split’s
rtol, so the WARNING does not fire for them.
Layback and position source#
Layback → layback_m and CableOut (+ CableOutHundredths)
→ cable_out_m, per ping, as written (some writers leave Layback
NaN or 0). position_source says whether latitude / longitude
already are the towfish: towfish when SensorX/Ycoordinate sit
more than SHIP_SENSOR_TOL_M from ShipX/Ycoordinate on at least
SHIP_SENSOR_FRACTION of the pings (the writer offset them), otherwise
unknown. The reader never infers ship: neither the format nor the
writers document it, so the call is left to a user who knows the system
as deployed (pass position_source="ship" to the layback step).
Ship equal to Sensor does not mean the ship: EdgeTech Discover writes the
laid-back towfish position in both fields. Files that look like ship
positions (written by a SHIP_POSITION_PROGRAMS program, no Ship
coordinates, Layback > 0 on at least SHIP_SENSOR_FRACTION of the
pings: Triton Isis records the layback without applying it) say so in
position_source_reason but stay unknown. The layback
correction step moves only ship positions.
One exception is detected and corrected. Some writers (seen in GeoDAS
files) offset Sensor from Ship by their tow-point offset and layback
rotated by the recorded heading. With no heading recorded
(SensorHeading 0 throughout) that offset points the same way, south
at heading 0, on every line whatever the course, so Sensor is not the
towfish. When SensorHeading is 0 on every ping, Sensor sits a fixed
east/north offset from Ship, and the line runs well away from north, the
Ship positions become latitude / longitude with
position_source ship, and the offset rotated back by heading 0
(less any recorded Layback) is recorded as tow_point_aft_m /
tow_point_starboard_m for the layback step. That step rotates by
heading_deg, so set a heading first (the course over ground,
course_over_ground()).
Not converted: pulse_length_us is not in the XTF ping or channel
headers and is 0.0 (as for SDF); gain codes are not interpreted.
Provenance attributes on every frequency dataset (xtf_ prefix):
recording program and version, sonar name and type, channel numbers
and names, whether sound velocity was doubled, the Weight range, and counts of reader
anomalies summed over the converted files (packets with fewer channel
blocks than declared, clipped channel blocks, channel blocks without
ChanInfo, unknown and non-sonar packets, resyncs, truncated files).
A single split run (XtfPingData.segment set) adds
xtf_segment_index and xtf_segment_count (1-based, kept runs of
the file), xtf_segment_ping_start / xtf_segment_ping_stop (its
half-open range in the file’s sonar pings), xtf_file_pings, and
xtf_runs_dropped_short / xtf_pings_dropped_short_runs (runs of
the file shorter than the split’s min_pings, and their pings).
2026 rshomberg@uri.edu
Contents
|
|
Relative tolerance for treating two pings' sample rates as equal. |
|
Sensor further than this (m) from Ship counts as a separate (towfish) position. |
|
Fraction of pings with a Ship position that must be apart for "towfish". |
|
Recording programs (lower-case name prefixes) whose Sensor coordinates, with no Ship coordinates and a recorded layback, look like the ship's position. |
|
Largest standard deviation (m) of a Sensor - Ship offset taken as fixed. |
|
Shortest Ship track (m) over which a fixed offset is judged. |
|
A fixed offset is only suspect on courses further than this from north. |
|
Convert XTF sonar pings into a validated SideScanSurvey. |
- sidescanpy.io.xtf.converter.HALVED_SOUND_VELOCITY_BELOW_MS = 1000.0#
SoundVelocityvalues below this (m/s) are taken as halved and doubled.
- sidescanpy.io.xtf.converter.SAMPLE_RATE_RTOL = 0.001#
Relative tolerance for treating two pings’ sample rates as equal.
- sidescanpy.io.xtf.converter.SHIP_SENSOR_TOL_M = 0.5#
Sensor further than this (m) from Ship counts as a separate (towfish) position.
- sidescanpy.io.xtf.converter.SHIP_SENSOR_FRACTION = 0.95#
Fraction of pings with a Ship position that must be apart for “towfish”.
- sidescanpy.io.xtf.converter.SHIP_POSITION_PROGRAMS = ('isis',)#
Recording programs (lower-case name prefixes) whose Sensor coordinates, with no Ship coordinates and a recorded layback, look like the ship’s position. Reported in position_source_reason only, never assumed.
- sidescanpy.io.xtf.converter.ZERO_HEADING_OFFSET_SD_M = 0.1#
Largest standard deviation (m) of a Sensor - Ship offset taken as fixed.
- sidescanpy.io.xtf.converter.ZERO_HEADING_MIN_TRACK_M = 20.0#
Shortest Ship track (m) over which a fixed offset is judged.
- sidescanpy.io.xtf.converter.ZERO_HEADING_MIN_COURSE_DEG = 20.0#
A fixed offset is only suspect on courses further than this from north.
- sidescanpy.io.xtf.converter.convert(pings, *, source_files=None, survey_name=None, nav_crs=None)#
Convert XTF sonar pings into a validated SideScanSurvey.
- Parameters:
- pingsXtfPingData or iterable of XtfPingData
Output of
sidescanpy.io.xtf.read_xtf(), one per file. Several files are merged into one survey: pings are concatenated in the given order and stably sorted by time if that order is not chronological; frequencies are matched by value and shorter pings NaN-padded to the longest.- source_fileslist[Path], optional
Source paths stored on the survey. Defaults to the paths in
pings.- survey_namestr, optional
Survey name. Defaults to the first source file’s stem.
- nav_crsint, str or pyproj.CRS, optional
Projected CRS of the coordinates in files with
NavUnits0 (easting / northing in metres), e.g.32619or"EPSG:32619"for UTM zone 19N. Required for such files; ignored for files withNavUnits3. Positions are converted to WGS84 longitude / latitude (see the module docstring).
- Returns:
- SideScanSurvey
One frequency channel per port/starboard frequency, navigation and attitude records from the ping headers, an empty DVL record.
- Raises:
- ValueError
If there are no sonar pings, no port/starboard channels, more than one channel pair of unknown frequency, a frequency whose sample rate cannot be derived, a file with
NavUnits0 and nonav_crs, or anav_crsthat is not projected.- NotImplementedError
If a file’s
NavUnitsis neither 0 nor 3.