sidescanpy.targets.correlate#
Resolve target positions and clock offsets against converted .ss.h5 files.
Catalog formats that store no absolute position (the Klein .tgt reader in
sidescanpy.io.sdf.targets) are located here against per-ping times of
converted surveys. For Klein catalogs use
resolve_targets_by_catalog_ping(), which matches the catalog’s own ping
time and number exactly; the pick-timestamp functions (with optional
constant clock-offset calibration) match the later save time and are only
a fallback for catalogs without those fields. Also
provides position-based correlation of any target to a ping in a directory of
converted surveys, and projection of a target onto another frequency channel.
2026 rshomberg@uri.edu
Contents
Approximate |
|
Derive the constant epoch-shift between each |
|
Resolve position (plus altitude/speed) for every target in |
|
Resolve position for every target in |
|
Place targets at the ping their catalog names (Klein |
|
Re-express |
|
Where a target falls in a set of converted surveys. |
|
Cache per-ping positions of every |
|
Pick the frequency key in |
|
Locate |
|
A correlated target and its position on every frequency of its file. |
|
|
|
Group |
- sidescanpy.targets.correlate.resolve_target_position(target, survey, freq_key, *, max_time_delta_s=300.0)#
Approximate
target’s position from the nearest ping (by timestamp) insurvey’sfreq_keychannel.This is a coarse nearest-ping lookup, not a stored pick position (no absolute lat/lon was found in
.tgt— see module docstring) and does not upgradeposition_confidenceabove"unknown".- Parameters:
- targetTarget
Must have
pick_timestampset.- surveySideScanSurvey
- freq_keystr
- max_time_delta_sfloat, default 300.0
If the nearest ping’s timestamp is farther than this from
target.pick_timestamp, position is left unset (targetreturned unchanged) rather than guessed from a clearly-wrong file.
- Returns:
- Target
Copy of
targetwithlatitude/longitudeset if a close-enough ping was found, otherwise unchanged.
- sidescanpy.targets.correlate.calibrate_clock_offset(ss_h5_dir, *, local_utc_offset_hours)#
Derive the constant epoch-shift between each
.ss.h5’s (possibly corrupted) ping clock and its filename-embedded true local start time.Filenames are expected to embed
<YYMMDD><HHMMSS>local start time (matches Klein SDF’s ownsonar_data<YYMMDD><HHMMSS>.sdfnaming, preserved through conversion). Returns the median offset (seconds, corrupted minus true) across every file found inss_h5_dir, logging each file’s individual estimate so a bad calibration (large spread => the offset isn’t actually constant for this survey) is visible rather than silently trusted.- Parameters:
- ss_h5_dirstr or Path
- local_utc_offset_hoursfloat
Required keyword. Local-time-to-UTC offset (hours to add to the filename’s embedded local time to get UTC). Use the offset that was actually in effect at the survey’s location/date – there is deliberately no default.
- Returns:
- float
Median offset in seconds (corrupted ping time - true start time).
- Raises:
- RuntimeError
If no file in
ss_h5_dirmatched the expected filename pattern.
- sidescanpy.targets.correlate.resolve_targets_offset_corrected(targets, ss_h5_dir, offset_s, *, max_time_delta_s=300.0)#
Resolve position (plus altitude/speed) for every target in
targetsusing a known constant clock offset (seecalibrate_clock_offset()).Enriches each matched target with
altitude_m/speed_mps/meters_per_ping/ground_range_minextra_attributes— all approximations from the nearest matched ping, not per-sample measurements, and constant across a target’s ~256-ping window.position_confidencestays"unknown"regardless of match quality, matchingresolve_target_position()’s convention. Targets with nopick_timestamp, or whose nearest match (across every.ss.h5file inss_h5_dir) is farther than20 * max_time_delta_saway, are returned unchanged.
- sidescanpy.targets.correlate.resolve_targets_nearest_ping(targets, ss_h5_dir, *, max_time_delta_s=300.0)#
Resolve position for every target in
targetsvia plain (no clock-offset) nearest-ping matching, checking every.ss.h5file and every frequency channel inss_h5_dirand keeping whichever match lands closest in time.Thin batch wrapper around
resolve_target_position()— useresolve_targets_offset_corrected()instead when the survey is known to need clock-offset correction (see that function andcalibrate_clock_offset()).
- sidescanpy.targets.correlate.resolve_targets_by_catalog_ping(targets, ss_h5_dir, *, max_time_delta_s=1.5)#
Place targets at the ping their catalog names (Klein
.tgt).Klein catalogs store the target ping’s own time and ping number (
extra_attributes["catalog_ping_time"]/["catalog_ping_number"], seesidescanpy.io.sdf.targets) on the sonar’s clock, the same clock as the converted pings. Matching them needs no clock offset and, unlikepick_timestamp(the later save time), identifies the target’s ping exactly.For each target, the pings of every
.ss.h5inss_h5_dirwithinmax_time_delta_sof the catalog time are searched for the catalog ping number (ping counters restart after recording gaps, so the time window is what makes the number unique). If the number is not there, the nearest ping in time within the window is used and recorded asposition_source="catalog_ping_time".Sets
latitude/longitudeto that ping’s position (the vehicle position at the target ping, not the target’s own seafloor position) andposition_confidence="approximate", and recordsss_h5(file stem),ping_index(file-local),ping_number,position_source("catalog_ping"),position_time_delta_s,altitude_m,ground_range_m/ground_range_clipped(fromrange_m), andspeed_mps/meters_per_pinginextra_attributes.- Parameters:
- targetslist of Target
- ss_h5_dirstr or Path
Directory of converted
.ss.h5files.- max_time_delta_sfloat, default 1.5
Half-width of the time window around the catalog time (the catalog time has 1 s resolution).
- Returns:
- list of Target
Same order as
targets. Targets without catalog fields, or whose catalog time is in no file (for example picked while not logging), are returned unchanged.
- sidescanpy.targets.correlate.project_target_to_frequency(target, source_mps, target_mps, *, n_samples=None)#
Re-express
target’s position on a different frequency channel of the same survey, using the one thing both channels share exactly: the physical world.A source catalog (EdgeTech Discover, Klein SonarWiz) only ever marks a contact against the one frequency channel it was picked on — there is no “also visible on the other channel” field. But
ping(both channels are pinged in lockstep — the same physical vessel position, identical ping count per channel within one file) and the physical slant range a sample represents (sample * meters_per_sample) are both real, channel-independent quantities. So the target’s true position on the untagged channel is recoverable: convert its sample index to a physical slant range using the source channel’s own meters/sample, then back to a sample index using the target channel’s — same ping, same side, no catalog entry needed for that channel at all.ping/channel/every other field are carried through unchanged (replace) — onlysamplemoves, plusextra_attributes["projected_from_mps"]recording the source resolution, so a caller/renderer can tell a real catalog mark apart from one recovered this way (e.g. to draw it with a different box style) without losing the fact that it’s the same real contact.Returns
None(not a clamped/guessed value) if the projected sample would land at or pastn_samples— a real, physical consequence of the two channels recording different maximum ranges, not a placement bug: the contact genuinely sits beyond the other channel’s recorded window and has no honest position to report there. Only checked whenn_samplesis given.
- class sidescanpy.targets.correlate.PositionMatch(path, freq_key, ping_idx, dist_m)#
Bases:
NamedTupleWhere a target falls in a set of converted surveys.
- Attributes:
- pathPath
The
.ss.h5file containing the matched ping.- freq_keystr
Frequency channel the match was made on.
- ping_idxint
Index along that file’s own ping axis (file-local, not the mission-global ping number).
- dist_mfloat
Distance from the target’s recorded position to the ping, in meters;
0.0for a match made fromTarget.pingalone.
- path#
Alias for field number 0
- freq_key#
Alias for field number 1
- ping_idx#
Alias for field number 2
- dist_m#
Alias for field number 3
- sidescanpy.targets.correlate.build_position_index(ss_h5_dir)#
Cache per-ping positions of every
.ss.h5inss_h5_dir.- Parameters:
- ss_h5_dirstr, Path, or sequence of them
Directory searched (non-recursively) for
*.ss.h5, or an explicit list of.ss.h5files (indexed in sorted order).
- Returns:
- dict
{path: {freq_key: info}}whereinfohaslat,lon(per-ping arrays),sample_interval_m(meters per sample, using 1500 m/s whensound_vel_msis not a positive finite value),n_samples,n_pingsandping_start(the file’s first value on the mission-global ping axis; nonzero for files that continue a global numbering). Surveys are read lazily so only the small position arrays are loaded.
- sidescanpy.targets.correlate.match_frequency(channels, target_mps)#
Pick the frequency key in
channelswhose sample interval is closest totarget_mps(meters per sample); the first key iftarget_mpsisNone.
- sidescanpy.targets.correlate.correlate_target(target, index, max_dist_m)#
Locate
targetin the surveys described byindex.The frequency channel is chosen per file with
match_frequency()usingtarget.extra_attributes["ping_meters_per_sample"]when present.Strategies are tried in order:
Resolved ping: a target placed by
resolve_targets_by_catalog_ping()records its file stem (extra_attributes["ss_h5"]) and file-localping_index; when that file is inindexand the ping in range, it is used as is (distance 0). Navigation fixes often repeat over several pings, so the nearest position alone can be several pings off.Position: the nearest ping by geodesic distance on the WGS84 ellipsoid (
pyproj.Geod.inv()) over all files, accepted if withinmax_dist_m. Pings without a position fix are skipped.Ping number (only if step 1 fails or the target has no lat/lon):
target.pingis a mission-global ping number, converted to a file-local index via each channel’sping_start. Accepted only when exactly one file’s range contains it; zero or several candidates returnNonerather than guess. This recovers catalogs that carry only a pixel-derived ping and no (or a less precise) GPS position.
- Parameters:
- targetTarget
- indexdict
From
build_position_index().- max_dist_mfloat
Maximum distance for a position match.
- Returns:
- PositionMatch or None
- class sidescanpy.targets.correlate.CatalogPlacement(target, match, on_frequency)#
Bases:
NamedTupleA correlated target and its position on every frequency of its file.
- Attributes:
- targetTarget
- matchPositionMatch
Where
correlate_target()placed it (its native frequency).- on_frequencydict
{freq_key: Target}– the target itself on its native frequency, and aproject_target_to_frequency()copy on every other frequency of the file whose recorded range contains it.
- target#
Alias for field number 0
- match#
Alias for field number 1
- on_frequency#
Alias for field number 2
- sidescanpy.targets.correlate.correlate_catalog(targets, index, max_dist_m)#
correlate_target()every target, projected onto every frequency of its file.Uncorrelated targets are omitted. Order follows
targets.- Parameters:
- targetssequence of Target
- indexdict
From
build_position_index().- max_dist_mfloat
Maximum distance for a position match.
- Returns:
- list of CatalogPlacement
- sidescanpy.targets.correlate.place_catalog(catalog, index, max_dist_m)#
Group
correlate_catalog()placements by file, frequency and channel.Targets without a
sampleare left out (they can’t be placed in a window).- Returns:
- dict
{(path, freq_key): {channel: [(target, file_ping_idx), ...]}}.