Transponders

Transponder message decoding: AIS (Automatic Identification System).

Transponder message decoding: AIS (Automatic Identification System).

Named after the MATLAB TCL’s Transponders/ directory, whose decodeAISString wraps libais; pytcl.transponders.ais plays the same role here over pyais (the ais extra). Importing this package never requires pyais to be installed – pyais is imported lazily inside decode_ais / ais_position_reports, raising DependencyError if it is missing when one of them is actually called.

Examples

>>> import pytcl.transponders as transponders
>>> vdm = "!AIVDM,1,1,,B,15M67FC000G?ufbE`FepT@3n00Sa,0*5C"
>>> msgs = transponders.decode_ais(vdm)
>>> msgs[0].msg_type
1

AIS

AIS NMEA sentence decoding and position-report extraction via pyais (ais extra).

AIS NMEA decoding via pyais, with position-report extraction.

pyais is an optional dependency (the ais extra). It is imported lazily inside _import_pyais, so importing this module never requires pyais to be installed; calling decode_ais or ais_position_reports without it raises DependencyError. This mirrors the guard pattern in pytcl.io.dataframes / pytcl.io.readers.

This is the Python port’s counterpart to the MATLAB TCL’s Transponders/decodeAISString, which wraps libais; here pyais plays that role.

Sentinel handling (ITU-R M.1371) is applied in ais_position_reports rather than left to pyais: empirically, pyais 3.2.1 does not normalize “not available” sentinels itself – it returns them unchanged (lat 91 deg, lon 181 deg, speed-over-ground 102.3 kn (1023 decideci-knots), course-over-ground 360.0 deg (3600 decidegrees), heading 511) – so this module detects the raw sentinel values coming back from pyais and converts each to NaN.

pytcl.transponders.ais.nmea_checksum(sentence)[source]

Compute an NMEA sentence’s *hh checksum.

The checksum is the XOR of every character strictly between the leading !/$ and the trailing *, rendered as two uppercase hex digits.

Parameters:

sentence (str) – A full sentence, with or without its trailing *hh.

Returns:

Two uppercase hex digits.

Return type:

str

Examples

>>> from pytcl.transponders.ais import nmea_checksum
>>> nmea_checksum("!AIVDM,1,1,,B,15M67FC000G?ufbE`FepT@3n00Sa,0*5C")
'5C'
class pytcl.transponders.ais.AISMessage(msg_type, mmsi, fields)[source]

Bases: NamedTuple

One decoded AIS message.

Variables:
  • msg_type (int) – ITU-R M.1371 message type (1-27).

  • mmsi (int) – Maritime Mobile Service Identity of the transmitting station.

  • fields (dict) – The pyais payload, normalized via its own asdict() – every field the message type carries, including type-specific ones (e.g. shipname for type 5) not surfaced by PositionReports.

msg_type: int

Alias for field number 0

mmsi: int

Alias for field number 1

fields: dict[str, Any]

Alias for field number 2

class pytcl.transponders.ais.PositionReports(mmsi, t, lat, lon, sog, cog, heading)[source]

Bases: NamedTuple

Position reports (msg types 1, 2, 3, 18, 19) as parallel arrays.

Variables:
  • mmsi (ndarray of int64, shape (n,))

  • t (ndarray of float64, shape (n,)) – Receiver timestamp per report, from times when given to ais_position_reports; NaN otherwise.

  • lat (ndarray of float64, shape (n,)) – Latitude, radians. NaN where pyais reports the ITU-R M.1371 “not available” sentinel (91 deg).

  • lon (ndarray of float64, shape (n,)) – Longitude, radians. NaN where pyais reports the sentinel (181 deg).

  • sog (ndarray of float64, shape (n,)) – Speed over ground, m/s (pyais reports knots; converted here). NaN where pyais reports the sentinel (102.3 kn).

  • cog (ndarray of float64, shape (n,)) – Course over ground, radians. NaN where pyais reports the sentinel (360.0 deg).

  • heading (ndarray of float64, shape (n,)) – True heading, radians. NaN where pyais reports the sentinel (511).

mmsi: ndarray[tuple[Any, ...], dtype[int64]]

Alias for field number 0

t: ndarray[tuple[Any, ...], dtype[float64]]

Alias for field number 1

lat: ndarray[tuple[Any, ...], dtype[float64]]

Alias for field number 2

lon: ndarray[tuple[Any, ...], dtype[float64]]

Alias for field number 3

sog: ndarray[tuple[Any, ...], dtype[float64]]

Alias for field number 4

cog: ndarray[tuple[Any, ...], dtype[float64]]

Alias for field number 5

heading: ndarray[tuple[Any, ...], dtype[float64]]

Alias for field number 6

pytcl.transponders.ais.decode_ais(nmea_text, validate_checksum=True)[source]

Decode AIS NMEA sentences (one or more, newline-separated) to messages.

Multipart messages (e.g. type 5, split across two !AIVDM sentences with the same sequence id) are reassembled automatically – pyais’s own ~pyais.stream.IterMessages groups fragments by sequence id, channel, talker and fragment count before assembling and decoding them, so a fragment is only turned into an AISMessage once every part of it has arrived.

Lines that are not valid AIS sentences, or whose payload pyais cannot decode (e.g. an unsupported message type), are skipped rather than raising – this is a batch decode over potentially noisy logs, not a single-message parse. The number skipped is logged at DEBUG (site "transponders") when diagnostics are enabled.

Parameters:
  • nmea_text (str) – One or more !AIVDM/!AIVDO sentences, one per line.

  • validate_checksum (bool, optional) – Reject sentences whose trailing *hh does not match the XOR of their body, and sentences carrying no *hh at all. Default True. Rejected lines are skipped and counted like any other undecodable line. Set False only to ingest a feed known to carry bad checksums, accepting that a corrupted position report may decode to a plausible but wrong latitude and longitude.

Returns:

One entry per successfully decoded (and, where applicable, reassembled) message, in the order completed.

Return type:

list of AISMessage

Raises:

DependencyError – If pyais is not installed.

Examples

>>> from pytcl.transponders.ais import decode_ais
>>> vdm = "!AIVDM,1,1,,B,15M67FC000G?ufbE`FepT@3n00Sa,0*5C"
>>> msgs = decode_ais(vdm)
>>> msgs[0].msg_type
1
>>> msgs[0].mmsi
366053209
pytcl.transponders.ais.ais_position_reports(nmea_text_or_messages, times=None, validate_checksum=True)[source]

Extract position reports (types 1, 2, 3, 18, 19) as parallel arrays.

Parameters:
  • nmea_text_or_messages (str or sequence of AISMessage) – Either raw NMEA text (decoded internally via decode_ais) or an already-decoded message list, e.g. from a prior decode_ais call.

  • times (sequence of float, optional) – One receiver timestamp per entry of nmea_text_or_messages (after decoding, if text was given) – times[i] is the timestamp for the i-th decoded message, not the i-th position report. When given, its length must equal the number of decoded messages. Entries whose message is not a position report are dropped along with that message. When omitted, PositionReports.t is all NaN.

  • validate_checksum (bool, optional) – Forwarded to decode_ais when raw text is given; ignored when an already-decoded message list is passed. Default True.

Returns:

One row per position-report message, in decode order. Units: lat/lon/cog/heading radians, sog m/s.

Return type:

PositionReports

Raises:
  • ValueError – If times is given and its length does not match the number of decoded messages.

  • DependencyError – If pyais is not installed.

Examples

>>> from pytcl.transponders.ais import ais_position_reports
>>> vdm = "!AIVDM,1,1,,B,15M67FC000G?ufbE`FepT@3n00Sa,0*5C"
>>> rep = ais_position_reports(vdm)
>>> rep.mmsi[0]
366053209
>>> bool(rep.lat[0] > 0)  # northern hemisphere
True