redvox.common.sensor_reader_utils API documentation
Description: This module loads sensor data from Redvox packets

# Module `redvox.common.sensor_reader_utils`

This module loads sensor data from Redvox packets

Expand source code

    """
    This module loads sensor data from Redvox packets
    """

    from typing import Callable, Dict, List, Optional, Tuple, Union

    import numpy as np

    # noinspection Mypy
    import pandas as pd

    import redvox.api1000.proto.redvox_api_m_pb2 as api_m
    from redvox.api1000.wrapped_redvox_packet.station_information import (
        NetworkType,
        PowerState,
        CellServiceState,
    )
    from redvox.common.stats_helper import StatsContainer
    from redvox.common import date_time_utils as dtu
    from redvox.common import gap_and_pad_utils as gpu
    from redvox.common.sensor_data import SensorType, SensorData

    # Dataframe column definitions
    COMPRESSED_AUDIO_COLUMNS: List[str] = [
        "timestamps",
        "unaltered_timestamps",
        "compressed_audio",
        "audio_codec",
    ]

    IMAGE_COLUMNS: List[str] = [
        "timestamps",
        "unaltered_timestamps",
        "image",
        "image_codec",
    ]

    LOCATION_COLUMNS: List[str] = [
        "timestamps",
        "unaltered_timestamps",
        "gps_timestamps",
        "latitude",
        "longitude",
        "altitude",
        "speed",
        "bearing",
        "horizontal_accuracy",
        "vertical_accuracy",
        "speed_accuracy",
        "bearing_accuracy",
        "location_provider",
    ]

    STATION_HEALTH_COLUMNS: List[str] = [
        "timestamps",
        "unaltered_timestamps",
        "battery_charge_remaining",
        "battery_current_strength",
        "internal_temp_c",
        "network_type",
        "network_strength",
        "power_state",
        "avail_ram",
        "avail_disk",
        "cell_service",
    ]

    # These are used for checking if a field is present or not
    __ACCELEROMETER_FIELD_NAME: str = "accelerometer"
    __AMBIENT_TEMPERATURE_FIELD_NAME: str = "ambient_temperature"
    __AUDIO_FIELD_NAME: str = "audio"
    __COMPRESSED_AUDIO_FIELD_NAME: str = "compressed_audio"
    __GRAVITY_FIELD_NAME: str = "gravity"
    __GYROSCOPE_FIELD_NAME: str = "gyroscope"
    __IMAGE_FIELD_NAME: str = "image"
    __LIGHT_FIELD_NAME: str = "light"
    __LINEAR_ACCELERATION_FIELD_NAME: str = "linear_acceleration"
    __LOCATION_FIELD_NAME: str = "location"
    __MAGNETOMETER_FIELD_NAME: str = "magnetometer"
    __ORIENTATION_FIELD_NAME: str = "orientation"
    __PRESSURE_FIELD_NAME: str = "pressure"
    __PROXIMITY_FIELD_NAME: str = "proximity"
    __RELATIVE_HUMIDITY_FIELD_NAME: str = "relative_humidity"
    __ROTATION_VECTOR: str = "rotation_vector"
    __VELOCITY: str = "velocity"

    __SENSOR_TYPE_TO_FIELD_NAME: Dict[SensorType, str] = {
        SensorType.UNKNOWN_SENSOR: "unknown",
        SensorType.STATION_HEALTH: "unknown",
        SensorType.ACCELEROMETER: __ACCELEROMETER_FIELD_NAME,
        SensorType.AMBIENT_TEMPERATURE: __AMBIENT_TEMPERATURE_FIELD_NAME,
        SensorType.AUDIO: __AUDIO_FIELD_NAME,
        SensorType.COMPRESSED_AUDIO: __COMPRESSED_AUDIO_FIELD_NAME,
        SensorType.GRAVITY: __GRAVITY_FIELD_NAME,
        SensorType.GYROSCOPE: __GYROSCOPE_FIELD_NAME,
        SensorType.IMAGE: __IMAGE_FIELD_NAME,
        SensorType.LIGHT: __LIGHT_FIELD_NAME,
        SensorType.LINEAR_ACCELERATION: __LINEAR_ACCELERATION_FIELD_NAME,
        SensorType.LOCATION: __LOCATION_FIELD_NAME,
        SensorType.BEST_LOCATION: __LOCATION_FIELD_NAME,
        SensorType.MAGNETOMETER: __MAGNETOMETER_FIELD_NAME,
        SensorType.ORIENTATION: __ORIENTATION_FIELD_NAME,
        SensorType.PRESSURE: __PRESSURE_FIELD_NAME,
        SensorType.PROXIMITY: __PROXIMITY_FIELD_NAME,
        SensorType.RELATIVE_HUMIDITY: __RELATIVE_HUMIDITY_FIELD_NAME,
        SensorType.ROTATION_VECTOR: __ROTATION_VECTOR,
        SensorType.INFRARED: __PROXIMITY_FIELD_NAME,
    }

    Sensor = Union[
        api_m.RedvoxPacketM.Sensors.Xyz,
        api_m.RedvoxPacketM.Sensors.Single,
        api_m.RedvoxPacketM.Sensors.Audio,
        api_m.RedvoxPacketM.Sensors.Image,
        api_m.RedvoxPacketM.Sensors.Location,
        api_m.RedvoxPacketM.Sensors.CompressedAudio,
    ]

    # Maps a sensor type to a function that can extract that sensor for a particular packet.
    __SENSOR_TYPE_TO_SENSOR_FN: Dict[
        SensorType,
        Optional[
            Callable[
                [api_m.RedvoxPacketM],
                Sensor,
            ]
        ],
    ] = {
        SensorType.UNKNOWN_SENSOR: None,
        SensorType.STATION_HEALTH: None,
        SensorType.ACCELEROMETER: lambda packet: packet.sensors.accelerometer,
        SensorType.AMBIENT_TEMPERATURE: lambda packet: packet.sensors.ambient_temperature,
        SensorType.AUDIO: lambda packet: packet.sensors.audio,
        SensorType.COMPRESSED_AUDIO: lambda packet: packet.sensors.compressed_audio,
        SensorType.GRAVITY: lambda packet: packet.sensors.gravity,
        SensorType.GYROSCOPE: lambda packet: packet.sensors.gyroscope,
        SensorType.IMAGE: lambda packet: packet.sensors.image,
        SensorType.LIGHT: lambda packet: packet.sensors.light,
        SensorType.LINEAR_ACCELERATION: lambda packet: packet.sensors.linear_acceleration,
        SensorType.LOCATION: lambda packet: packet.sensors.location,
        SensorType.BEST_LOCATION: lambda packet: packet.sensors.location,
        SensorType.MAGNETOMETER: lambda packet: packet.sensors.magnetometer,
        SensorType.ORIENTATION: lambda packet: packet.sensors.orientation,
        SensorType.PRESSURE: lambda packet: packet.sensors.pressure,
        SensorType.PROXIMITY: lambda packet: packet.sensors.proximity,
        SensorType.RELATIVE_HUMIDITY: lambda packet: packet.sensors.relative_humidity,
        SensorType.ROTATION_VECTOR: lambda packet: packet.sensors.rotation_vector,
        SensorType.INFRARED: lambda packet: packet.sensors.proximity,
    }

    def __has_sensor(
            data: Union[api_m.RedvoxPacketM, api_m.RedvoxPacketM.Sensors], field_name: str
    ) -> bool:
        """
        Returns true if the given packet or sensors instance contains the valid sensor.

        :param data: Either a packet or a packet's sensors message.
        :param field_name: The name of the sensor being checked.
        :return: True if the sensor exists, False otherwise.
        """
        if isinstance(data, api_m.RedvoxPacketM):
            # noinspection Mypy,PyTypeChecker
            return data.sensors.HasField(field_name)

        if isinstance(data, api_m.RedvoxPacketM.Sensors):
            # noinspection Mypy,PyTypeChecker
            return data.HasField(field_name)

        return False

    def __packet_duration_s(packet: api_m.RedvoxPacketM) -> float:
        """
        Returns the packet duration in seconds.

        :param packet: The packet to calculate the duration for.
        :return: The packet duration in seconds.
        """
        return len(packet.sensors.audio.samples.values) / packet.sensors.audio.sample_rate

    def __packet_duration_us(packet: api_m.RedvoxPacketM) -> float:
        """
        Returns the packet duration in microseconds.

        :param packet: The packet to calculate the duration for.
        :return: The packet duration in microseconds.
        """
        return __packet_duration_s(packet) * 1_000_000.0

    def __stats_for_sensor_per_packet_per_second(num_packets: int,
                                                 packet_dur_s: float,
                                                 timestamps: np.array) -> Tuple[float, float, float]:
        """
        Sensor being evaluated must either have 1/packet or 1/second sample rate

        :param num_packets: number of packets to calculate stats for
        :param packet_dur_s: duration of packet in seconds
        :param timestamps: timestamps of the samples
        :return: sample rate in hz, sample interval in seconds, and sample interval std deviation
        """
        if len(timestamps) != num_packets:
            sample_rate = 1.0
        else:
            sample_rate = 1 / packet_dur_s
        sample_interval = 1 / sample_rate
        sample_interval_std = (
            dtu.microseconds_to_seconds(float(np.std(np.diff(timestamps))))
            if len(timestamps) > 1
            else np.nan
        )
        return sample_rate, sample_interval, sample_interval_std

    def get_empty_sensor_data(
            name: str, sensor_type: SensorType = SensorType.UNKNOWN_SENSOR
    ) -> SensorData:
        """
        create a sensor data object with no data

        :param name: name of the sensor
        :param sensor_type: type of the sensor to create, default SensorType.UNKNOWN_SENSOR
        :return: empty sensor
        """
        return SensorData(name, pd.DataFrame([], columns=["timestamps"]), sensor_type)

    def get_sensor_description(sensor: Sensor) -> str:
        """
        read the sensor's description from the sensor

        :param sensor: the sensor to read the description from
        :return: the sensor's description
        """
        return sensor.sensor_description

    def get_sensor_description_list(
            packets: List[api_m.RedvoxPacketM], sensor_type: SensorType
    ) -> Optional[str]:
        """
        read the sensor_type sensor's description from a list of packets

        :param packets: the list of packets to read from
        :param sensor_type: the SensorType of the sensor to read the description of
        :return: the sensor_type sensor's description
        """

        field_name: str = __SENSOR_TYPE_TO_FIELD_NAME[sensor_type]
        sensor_fn: Optional[
            Callable[[api_m.RedvoxPacketM], Sensor]
        ] = __SENSOR_TYPE_TO_SENSOR_FN[sensor_type]
        for packet in packets:
            if __has_sensor(packet, field_name) and sensor_fn is not None:
                return sensor_fn(packet).sensor_description

        return None

    def get_sample_statistics(data_df: pd.DataFrame) -> Tuple[float, float, float]:
        """
        calculate the sample rate, interval and interval std dev using the timestamps in the dataframe

        :param data_df: the dataframe containing timestamps to calculate statistics from
        :return: a Tuple containing the sample rate, interval and interval std dev
        """
        sample_interval: float
        sample_interval_std: float
        if data_df["timestamps"].size > 1:
            sample_interval = dtu.microseconds_to_seconds(
                float(np.mean(np.diff(data_df["timestamps"])))
            )
            sample_interval_std = dtu.microseconds_to_seconds(
                float(np.std(np.diff(data_df["timestamps"])))
            )
        else:
            sample_interval = np.nan
            sample_interval_std = np.nan
        return 1.0 / sample_interval, sample_interval, sample_interval_std

    def read_apim_xyz_sensor(
            sensor: api_m.RedvoxPacketM.Sensors.Xyz, column_id: str
    ) -> pd.DataFrame:
        """
        read a sensor that has xyz data channels from an api M data packet
        raises Attribute Error if sensor does not contain xyz channels

        :param sensor: the xyz api M sensor to read
        :param column_id: string, used to name the columns
        :return: Dataframe representing the data in the sensor
        """
        timestamps: np.ndarray = np.array(sensor.timestamps.timestamps)
        try:
            columns: List[str] = [
                "timestamps",
                "unaltered_timestamps",
                f"{column_id}_x",
                f"{column_id}_y",
                f"{column_id}_z",
            ]
            return pd.DataFrame(
                np.transpose(
                    [
                        timestamps,
                        timestamps,
                        np.array(sensor.x_samples.values),
                        np.array(sensor.y_samples.values),
                        np.array(sensor.z_samples.values),
                    ]
                ),
                columns=columns,
            )
        except AttributeError:
            raise

    def load_apim_xyz_sensor_from_list(
            sensor_type: SensorType,
            data: List[List[float]],
            gaps: List[Tuple[float, float]],
            column_name: str,
            description: str,
            sample_interval_micros: float,
    ) -> Optional[SensorData]:
        """
        create a three channel sensor of sensor_type with the column name, description, and data given.

        :param sensor_type: the SensorType of sensor to create
        :param data: the list of data to be added; requires timestamps to be the first list
        :param gaps: the list of non-inclusive start and end times of the gaps in the packets
        :param column_name: the name of the columns that contain the data
        :param description: the description of the sensor
        :param sample_interval_micros: known sample interval in microseconds
        :return: SensorData object or None if no data
        """
        if len(data[0]) > 0:
            data_df: pd.DataFrame = pd.DataFrame(
                np.transpose([data[0], data[0], data[1], data[2], data[3]]),
                columns=[
                    "timestamps",
                    "unaltered_timestamps",
                    f"{column_name}_x",
                    f"{column_name}_y",
                    f"{column_name}_z",
                ],
            )
            return SensorData(
                description,
                gpu.fill_gaps(data_df, gaps, sample_interval_micros, True),
                sensor_type,
                calculate_stats=True,
            )
        return None

    def read_apim_single_sensor(
            sensor: api_m.RedvoxPacketM.Sensors.Single, column_id: str
    ) -> pd.DataFrame:
        """
        read a sensor that has a single data channel from an api M data packet
        raises Attribute Error if sensor does not contain exactly one data channel

        :param sensor: the single channel api M sensor to read
        :param column_id: string, used to name the columns
        :return: Dataframe representing the data in the sensor
        """
        timestamps: np.ndarray = np.array(sensor.timestamps.timestamps)
        try:
            columns: List[str] = ["timestamps", "unaltered_timestamps", column_id]
            return pd.DataFrame(
                np.transpose([timestamps, timestamps, np.array(sensor.samples.values)]),
                columns=columns,
            )
        except AttributeError:
            raise

    def load_apim_single_sensor_from_list(
            sensor_type: SensorType,
            timestamps: List[float],
            data: List[float],
            gaps: List[Tuple[float, float]],
            column_name: str,
            description: str,
            sample_interval_micros: float,
    ) -> Optional[SensorData]:
        """
        Create a single channel sensor of sensor_type with the column name, timestamps, data, and description

        :param sensor_type: the SensorType of sensor to create
        :param timestamps: the timestamps of the data
        :param data: the list of data to be added
        :param gaps: the list of non-inclusive start and end times of the gaps in the packets
        :param column_name: the name of the columns that contain the data
        :param description: the description of the sensor
        :param sample_interval_micros: known sample interval in microseconds
        :return:
        """
        if len(timestamps) > 0:
            data_df: pd.DataFrame = pd.DataFrame(
                np.transpose([timestamps, timestamps, data]),
                columns=["timestamps", "unaltered_timestamps", column_name],
            )
            return SensorData(
                description,
                gpu.fill_gaps(data_df, gaps, sample_interval_micros, True),
                sensor_type,
                calculate_stats=True,
            )
        return None

    def load_apim_audio(packet: api_m.RedvoxPacketM) -> Optional[SensorData]:
        """
        load audio data from a single redvox packet

        :param packet: packet with data to load
        :return: audio sensor data if it exists, None otherwise
        """
        if __has_sensor(packet, __AUDIO_FIELD_NAME):
            audio_sensor: api_m.RedvoxPacketM.Sensors.Audio = packet.sensors.audio

            timestamps: np.ndarray = gpu.calc_evenly_sampled_timestamps(
                audio_sensor.first_sample_timestamp,
                len(audio_sensor.samples.values),
                dtu.seconds_to_microseconds(1.0 / audio_sensor.sample_rate),
            )

            return SensorData(
                audio_sensor.sensor_description,
                pd.DataFrame(
                    np.transpose(
                        [timestamps, timestamps, np.array(audio_sensor.samples.values)]
                    ),
                    columns=gpu.AUDIO_DF_COLUMNS,
                ),
                SensorType.AUDIO,
                audio_sensor.sample_rate,
                1.0 / audio_sensor.sample_rate,
                0.0,
                True,
                )

        return None

    def load_apim_audio_from_list(
            packets: List[api_m.RedvoxPacketM],
    ) -> Tuple[Optional[SensorData], List[Tuple[float, float]]]:
        """
        load audio data from a list of redvox packets
        NOTE: This only works because audio sensors in the list should all have the same number of data points.

        :param packets: packets with data to load
        :return: audio sensor data if it exists, None otherwise
        """
        if len(packets) > 0:
            if __has_sensor(
                    packets[0], __AUDIO_FIELD_NAME
            ):
                sample_rate_hz: float = packets[0].sensors.audio.sample_rate
                packet_info = [
                    (
                        p.sensors.audio.first_sample_timestamp,
                        np.array(p.sensors.audio.samples.values),
                    )
                    for p in packets
                ]
                gp_result = gpu.fill_audio_gaps(
                    packet_info, dtu.seconds_to_microseconds(1 / sample_rate_hz)
                )
                sensor_data = SensorData(
                    get_sensor_description_list(packets, SensorType.AUDIO),
                    gp_result.result_df,
                    SensorType.AUDIO,
                    sample_rate_hz,
                    1 / sample_rate_hz,
                    0.0,
                    True,
                    )
                if len(gp_result.errors.get()) > 0:
                    sensor_data.errors.extend_error(gp_result.errors)

                return (
                    sensor_data,
                    gp_result.gaps,
                )
        return None, []

    def load_apim_compressed_audio(packet: api_m.RedvoxPacketM) -> Optional[SensorData]:
        """
        load compressed audio data from a single redvox packet

        :param packet: packet with data to load
        :return: compressed audio sensor data if it exists, None otherwise
        """
        if __has_sensor(packet, __COMPRESSED_AUDIO_FIELD_NAME):
            comp_audio: api_m.RedvoxPacketM.Sensors.CompressedAudio = (
                packet.sensors.compressed_audio
            )
            sample_rate_hz = comp_audio.sample_rate
            return SensorData(
                comp_audio.sensor_description,
                pd.DataFrame(
                    np.transpose(
                        [
                            comp_audio.first_sample_timestamp,
                            comp_audio.first_sample_timestamp,
                            np.array(list(comp_audio.audio_bytes)),
                            comp_audio.audio_codec,
                        ]
                    ),
                    columns=COMPRESSED_AUDIO_COLUMNS,
                ),
                SensorType.COMPRESSED_AUDIO,
                sample_rate_hz,
                1 / sample_rate_hz,
                0.0,
                True,
                )
        return None

    def load_apim_compressed_audio_from_list(
            packets: List[api_m.RedvoxPacketM], gaps: List[Tuple[float, float]]
    ) -> Optional[SensorData]:
        """
        load compressed audio data from a list of redvox packets

        :param packets: packets with data to load
        :param gaps: the list of non-inclusive start and end times of the gaps in the packets
        :return: compressed audio sensor data if it exists, None otherwise
        """
        data_list = [[], [], []]
        for packet in packets:
            if __has_sensor(packet, __COMPRESSED_AUDIO_FIELD_NAME):
                comp_audio = packet.sensors.compressed_audio
                data_list[0].append(comp_audio.first_sample_timestamp)
                data_list[1].append(comp_audio.audio_bytes)
                data_list[2].append(comp_audio.audio_codec)

        if len(data_list[0]) > 0:
            data_df = gpu.fill_gaps(
                pd.DataFrame(
                    np.transpose([data_list[0], data_list[0], data_list[1], data_list[2]]),
                    columns=COMPRESSED_AUDIO_COLUMNS,
                ),
                gaps,
                __packet_duration_us(packets[0]),
                True,
            )
            data_df["audio_codec"] = [d for d in data_df["audio_codec"]]
            sample_rate_hz = packets[0].sensors.compressed_audio.sample_rate
            return SensorData(
                get_sensor_description_list(packets, SensorType.COMPRESSED_AUDIO),
                data_df,
                SensorType.COMPRESSED_AUDIO,
                sample_rate_hz,
                1 / sample_rate_hz,
                0.0,
                True,
                )
        return None

    def load_apim_image(packet: api_m.RedvoxPacketM) -> Optional[SensorData]:
        """
        load image data from a single redvox packet

        :param packet: packet with data to load
        :return: image sensor data if it exists, None otherwise
        """
        if __has_sensor(packet, __IMAGE_FIELD_NAME):
            image_sensor: api_m.RedvoxPacketM.Sensors.Image = packet.sensors.image
            timestamps = image_sensor.timestamps.timestamps
            codecs = np.full(len(timestamps), image_sensor.image_codec)
            data_df = pd.DataFrame(
                np.transpose([timestamps, timestamps, image_sensor.samples, codecs]),
                columns=IMAGE_COLUMNS,
            )
            data_df["image_codec"] = [d for d in data_df["image_codec"]]
            # image is collected 1 per packet or 1 per second
            sample_rate, sample_interval, sample_interval_std = __stats_for_sensor_per_packet_per_second(
                1, __packet_duration_s(packet), timestamps
            )
            return SensorData(
                image_sensor.sensor_description,
                data_df,
                SensorType.IMAGE,
                sample_rate,
                sample_interval,
                sample_interval_std,
                True,
            )
        return None

    def load_apim_image_from_list(
            packets: List[api_m.RedvoxPacketM], gaps: List[Tuple[float, float]]
    ) -> Optional[SensorData]:
        """
        load image data from a list of redvox packets

        :param packets: packets with data to load
        :param gaps: the list of non-inclusive start and end times of the gaps in the packets
        :return: image sensor data if it exists, None otherwise
        """
        data_list: List[List[float]] = [[], [], []]
        for packet in packets:
            if __has_sensor(packet, __IMAGE_FIELD_NAME):
                image_sensor = packet.sensors.image

                data_list[0].extend(image_sensor.timestamps.timestamps)
                data_list[1].extend(image_sensor.samples)
                data_list[2].extend(
                    [
                        image_sensor.image_codec
                        for i in range(len(image_sensor.timestamps.timestamps))
                    ]
                )
        if len(data_list[0]) > 0:
            # image is collected 1 per packet or 1 per second
            sample_rate, sample_interval, sample_interval_std = __stats_for_sensor_per_packet_per_second(
                len(packets), __packet_duration_s(packets[0]), data_list[0]
            )
            df = pd.DataFrame(
                np.transpose(
                    [data_list[0], data_list[0], data_list[1], data_list[2]]
                ),
                columns=IMAGE_COLUMNS,
            )
            df["timestamps"] = df["timestamps"].astype(float)
            df["unaltered_timestamps"] = df["unaltered_timestamps"].astype(float)
            df["image_codec"] = df["image_codec"].astype(float)
            return SensorData(
                get_sensor_description_list(packets, SensorType.IMAGE),
                gpu.fill_gaps(
                    df,
                    gaps,
                    dtu.seconds_to_microseconds(sample_interval),
                    True,
                ),
                SensorType.IMAGE,
                sample_rate,
                sample_interval,
                sample_interval_std,
                True,
            )
        return None

    def __is_only_best_values(loc: api_m.RedvoxPacketM.Sensors.Location) -> bool:
        """
        :return: True if the location does not have data in it and has a last_best_location or overall_best_location
        """
        return len(loc.location_providers) < 1 and (
                loc.HasField("last_best_location") or loc.HasField("overall_best_location")
        )

    def load_apim_best_location(packet: api_m.RedvoxPacketM) -> Optional[SensorData]:
        """
        load best location data from a single redvox packet

        :param packet: packet with data to load
        :return: best location sensor data if it exists, None otherwise
        """
        if __has_sensor(packet, __LOCATION_FIELD_NAME):
            loc: api_m.RedvoxPacketM.Sensors.Location = packet.sensors.location
            if loc.HasField("last_best_location") or loc.HasField("overall_best_location"):
                best_loc: api_m.RedvoxPacketM.Sensors.Location.BestLocation
                if loc.HasField("last_best_location"):
                    best_loc = loc.last_best_location
                else:
                    best_loc = loc.overall_best_location
                data_df = pd.DataFrame([
                    [
                        packet.timing_information.packet_start_mach_timestamp,
                        best_loc.latitude_longitude_timestamp.mach,
                        best_loc.latitude_longitude_timestamp.gps,
                        best_loc.latitude,
                        best_loc.longitude,
                        best_loc.altitude,
                        best_loc.speed,
                        best_loc.bearing,
                        best_loc.horizontal_accuracy,
                        best_loc.vertical_accuracy,
                        best_loc.speed_accuracy,
                        best_loc.bearing_accuracy,
                        best_loc.location_provider,
                    ]
                ], columns=LOCATION_COLUMNS)
                sample_rate = 1 / __packet_duration_s(packet)
                return SensorData(
                    loc.sensor_description,
                    data_df,
                    SensorType.BEST_LOCATION,
                    sample_rate,
                    1 / sample_rate,
                    np.nan,
                    False,
                )
        return None

    def load_apim_best_location_from_list(
            packets: List[api_m.RedvoxPacketM], gaps: List[Tuple[float, float]]
    ) -> Optional[SensorData]:
        """
        load best location data from a list of redvox packets

        :param packets: packets with data to load
        :param gaps: the list of non-inclusive start and end times of the gaps in the packets
        :return: best location sensor data if it exists, None otherwise
        """
        data_list: List[List[float]] = [[], [], [], [], [], [], [], [], [], [], [], [], []]
        loc_stats = StatsContainer("location_sensor")
        for packet in packets:
            if __has_sensor(packet, __LOCATION_FIELD_NAME):
                loc: api_m.RedvoxPacketM.Sensors.Location = packet.sensors.location
                if loc.HasField("last_best_location") or loc.HasField("overall_best_location"):
                    best_loc: api_m.RedvoxPacketM.Sensors.Location.BestLocation
                    if loc.HasField("last_best_location"):
                        best_loc = loc.last_best_location
                    else:
                        best_loc = loc.overall_best_location
                    data_list[0].append(packet.timing_information.packet_start_mach_timestamp)
                    data_list[1].append(best_loc.latitude_longitude_timestamp.mach)
                    data_list[2].append(best_loc.latitude_longitude_timestamp.gps)
                    data_list[3].append(best_loc.latitude)
                    data_list[4].append(best_loc.longitude)
                    data_list[5].append(best_loc.altitude)
                    data_list[6].append(best_loc.speed)
                    data_list[7].append(best_loc.bearing)
                    data_list[8].append(best_loc.horizontal_accuracy)
                    data_list[9].append(best_loc.vertical_accuracy)
                    data_list[10].append(best_loc.speed_accuracy)
                    data_list[11].append(best_loc.bearing_accuracy)
                    data_list[12].append(best_loc.location_provider)
                    loc_stats.add(__packet_duration_us(packet), 0, 1)
        if len(data_list[0]) > 0:
            return SensorData(
                get_sensor_description_list(packets, SensorType.BEST_LOCATION),
                gpu.fill_gaps(
                    pd.DataFrame(np.transpose(data_list), columns=LOCATION_COLUMNS),
                    gaps,
                    loc_stats.mean_of_means(),
                    True,
                ),
                SensorType.BEST_LOCATION,
                calculate_stats=True,
            )

    def load_apim_location(packet: api_m.RedvoxPacketM) -> Optional[SensorData]:
        """
        load location data from a single packet

        :param packet: packet with data to load
        :return: location sensor data if it exists, None otherwise
        """
        if __has_sensor(packet, __LOCATION_FIELD_NAME):
            loc: api_m.RedvoxPacketM.Sensors.Location = packet.sensors.location
            timestamps = loc.timestamps.timestamps
            if len(timestamps) > 0:
                gps_timestamps = loc.timestamps_gps.timestamps
                lat_samples = loc.latitude_samples.values
                lon_samples = loc.longitude_samples.values
                alt_samples = loc.altitude_samples.values
                spd_samples = loc.speed_samples.values
                bear_samples = loc.bearing_samples.values
                hor_acc_samples = loc.horizontal_accuracy_samples.values
                vert_acc_samples = loc.vertical_accuracy_samples.values
                spd_acc_samples = loc.speed_accuracy_samples.values
                bear_acc_samples = loc.bearing_accuracy_samples.values
                loc_prov_samples = loc.location_providers
                data_for_df = []
                for i in range(len(timestamps)):
                    new_entry = [
                        timestamps[i],
                        timestamps[i],
                        np.nan if len(gps_timestamps) <= i else gps_timestamps[i],
                        lat_samples[i],
                        lon_samples[i],
                        np.nan if len(alt_samples) <= i else alt_samples[i],
                        np.nan if len(spd_samples) <= i else spd_samples[i],
                        np.nan if len(bear_samples) <= i else bear_samples[i],
                        np.nan if len(hor_acc_samples) <= i else hor_acc_samples[i],
                        np.nan if len(vert_acc_samples) <= i else vert_acc_samples[i],
                        np.nan if len(spd_acc_samples) <= i else spd_acc_samples[i],
                        np.nan if len(bear_acc_samples) <= i else bear_acc_samples[i],
                        np.nan if len(loc_prov_samples) <= i else loc_prov_samples[i],
                    ]
                    data_for_df.append(new_entry)
            else:
                return None
            data_df = pd.DataFrame(data_for_df, columns=LOCATION_COLUMNS)
            sample_rate, sample_interval, sample_interval_std = get_sample_statistics(
                data_df
            )
            return SensorData(
                loc.sensor_description,
                data_df,
                SensorType.LOCATION,
                sample_rate,
                sample_interval,
                sample_interval_std,
                False,
            )
        return None

    def load_apim_location_from_list(
            packets: List[api_m.RedvoxPacketM], gaps: List[Tuple[float, float]]
    ) -> Optional[SensorData]:
        """
        load location data from a list of redvox packets

        :param packets: packets with data to load
        :param gaps: the list of non-inclusive start and end times of the gaps in the packets
        :return: location sensor data if it exists, None otherwise
        """
        data_list: List[List[float]] = [[], [], [], [], [], [], [], [], [], [], [], []]
        loc_stats = StatsContainer("location_sensor")
        for packet in packets:
            if __has_sensor(packet, __LOCATION_FIELD_NAME):
                loc: api_m.RedvoxPacketM.Sensors.Location = packet.sensors.location

                num_samples = len(loc.timestamps.timestamps)
                if num_samples > 0:
                    samples = loc.timestamps.timestamps
                    data_list[0].extend(samples)
                    if num_samples == 1:
                        loc_stats.add(
                            __packet_duration_us(packet),
                            0,
                            1,
                        )
                    else:
                        loc_stats.add(
                            np.mean(np.diff(samples)),
                            np.std(np.diff(samples)),
                            num_samples - 1,
                            )
                    for i in range(num_samples):
                        samples = loc.timestamps_gps.timestamps
                        data_list[1].append(np.nan if len(samples) <= i else samples[i])
                        samples = loc.latitude_samples.values
                        data_list[2].append(samples[i])
                        samples = loc.longitude_samples.values
                        data_list[3].append(samples[i])
                        samples = loc.altitude_samples.values
                        data_list[4].append(
                            np.nan if len(samples) < i + 1 else samples[i]
                        )
                        samples = loc.speed_samples.values
                        data_list[5].append(
                            np.nan if len(samples) < i + 1 else samples[i]
                        )
                        samples = loc.bearing_samples.values
                        data_list[6].append(
                            np.nan if len(samples) < i + 1 else samples[i]
                        )
                        samples = loc.horizontal_accuracy_samples.values
                        data_list[7].append(
                            np.nan if len(samples) < i + 1 else samples[i]
                        )
                        samples = loc.vertical_accuracy_samples.values
                        data_list[8].append(
                            np.nan if len(samples) < i + 1 else samples[i]
                        )
                        samples = loc.speed_accuracy_samples.values
                        data_list[9].append(
                            np.nan if len(samples) < i + 1 else samples[i]
                        )
                        samples = loc.bearing_accuracy_samples.values
                        data_list[10].append(
                            np.nan if len(samples) < i + 1 else samples[i]
                        )
                        samples = list(loc.location_providers)
                        data_list[11].append(
                            api_m.RedvoxPacketM.Sensors.Location.LocationProvider.UNKNOWN
                            if len(samples) < i + 1
                            else samples[i]
                        )
        if len(data_list[0]) > 0:
            data_list.insert(1, data_list[0].copy())
            return SensorData(
                get_sensor_description_list(packets, SensorType.LOCATION),
                gpu.fill_gaps(
                    pd.DataFrame(np.transpose(data_list), columns=LOCATION_COLUMNS),
                    gaps,
                    loc_stats.mean_of_means(),
                    True,
                ),
                SensorType.LOCATION,
                calculate_stats=True,
            )
        return None

    def load_single(
            packet: api_m.RedvoxPacketM,
            sensor_type: SensorType,
    ) -> Optional[SensorData]:
        field_name: str = __SENSOR_TYPE_TO_FIELD_NAME[sensor_type]
        sensor_fn: Optional[
            Callable[[api_m.RedvoxPacketM], Sensor]
        ] = __SENSOR_TYPE_TO_SENSOR_FN[sensor_type]
        if __has_sensor(packet, field_name) and sensor_fn is not None:
            sensor = sensor_fn(packet)
            data_df = read_apim_single_sensor(sensor, field_name)
            sample_rate, sample_interval, sample_interval_std = get_sample_statistics(
                data_df
            )
            return SensorData(
                sensor.sensor_description,
                data_df,
                sensor_type,
                sample_rate,
                sample_interval,
                sample_interval_std,
                False,
            )

    def load_single_from_list(
            packets: List[api_m.RedvoxPacketM],
            gaps: List[Tuple[float, float]],
            sensor_type: SensorType,
    ) -> Optional[SensorData]:
        field_name: str = __SENSOR_TYPE_TO_FIELD_NAME[sensor_type]
        data_list: List[float] = []
        timestamps: List[float] = []
        sensor_stats: StatsContainer = StatsContainer(f"{field_name}_sensor")
        sensor_fn: Optional[
            Callable[[api_m.RedvoxPacketM], Sensor]
        ] = __SENSOR_TYPE_TO_SENSOR_FN[sensor_type]
        for packet in packets:
            if __has_sensor(packet, field_name) and sensor_fn is not None:
                sensor: api_m.RedvoxPacketM.Sensors.Single = sensor_fn(packet)
                data_list.extend(sensor.samples.values)
                ts = sensor.timestamps.timestamps
                timestamps.extend(ts)
                if len(sensor.timestamps.timestamps) == 1:
                    sensor_stats.add(__packet_duration_us(packet), 0, 1)
                else:
                    sensor_stats.add(np.mean(np.diff(ts)), np.std(np.diff(ts)), len(ts) - 1)
        if len(data_list) > 0:
            return load_apim_single_sensor_from_list(
                sensor_type,
                timestamps,
                data_list,
                gaps,
                field_name,
                get_sensor_description_list(packets, sensor_type),
                sensor_stats.mean_of_means(),
            )
        return None

    def load_apim_pressure(packet: api_m.RedvoxPacketM) -> Optional[SensorData]:
        """
        load pressure data from a single redvox packet

        :param packet: packet with data to load
        :return: pressure sensor data if it exists, None otherwise
        """
        return load_single(packet, SensorType.PRESSURE)

    def load_apim_pressure_from_list(
            packets: List[api_m.RedvoxPacketM], gaps: List[Tuple[float, float]]
    ) -> Optional[SensorData]:
        """
        load pressure data from a list of redvox packets

        :param packets: packets with data to load
        :param gaps: the list of non-inclusive start and end times of the gaps in the packets
        :return: pressure sensor data if it exists, None otherwise
        """
        return load_single_from_list(
            packets,
            gaps,
            SensorType.PRESSURE,
        )

    def load_apim_light(packet: api_m.RedvoxPacketM) -> Optional[SensorData]:
        """
        load light data from a single redvox packet

        :param packet: packet with data to load
        :return: light sensor data if it exists, None otherwise
        """
        return load_single(packet, SensorType.LIGHT)

    def load_apim_light_from_list(
            packets: List[api_m.RedvoxPacketM], gaps: List[Tuple[float, float]]
    ) -> Optional[SensorData]:
        """
        load light data from a list of redvox packets

        :param packets: packets with data to load
        :param gaps: the list of non-inclusive start and end times of the gaps in the packets
        :return: light sensor data if it exists, None otherwise
        """
        return load_single_from_list(
            packets,
            gaps,
            SensorType.LIGHT,
        )

    def load_apim_proximity(packet: api_m.RedvoxPacketM) -> Optional[SensorData]:
        """
        load proximity data from a single redvox packet

        :param packet: packet with data to load
        :return: proximity sensor data if it exists, None otherwise
        """
        return load_single(packet, SensorType.PROXIMITY)

    def load_apim_proximity_from_list(
            packets: List[api_m.RedvoxPacketM], gaps: List[Tuple[float, float]]
    ) -> Optional[SensorData]:
        """
        load proximity data from a list of redvox packets

        :param packets: packets with data to load
        :param gaps: the list of non-inclusive start and end times of the gaps in the packets
        :return: proximity sensor data if it exists, None otherwise
        """
        return load_single_from_list(
            packets,
            gaps,
            SensorType.PROXIMITY,
        )

    def load_apim_ambient_temp(
            packet: api_m.RedvoxPacketM,
    ) -> Optional[SensorData]:
        """
        load ambient temperature data from a single redvox packet

        :param packet: packet with data to load
        :return: ambient temperature sensor data if it exists, None otherwise
        """
        return load_single(
            packet,
            SensorType.AMBIENT_TEMPERATURE,
        )

    def load_apim_ambient_temp_from_list(
            packets: List[api_m.RedvoxPacketM], gaps: List[Tuple[float, float]]
    ) -> Optional[SensorData]:
        """
        load ambient temperature data from a list of redvox packets

        :param packets: packets with data to load
        :param gaps: the list of non-inclusive start and end times of the gaps in the packets
        :return: ambient temperature sensor data if it exists, None otherwise
        """
        return load_single_from_list(
            packets,
            gaps,
            SensorType.AMBIENT_TEMPERATURE,
        )

    def load_apim_rel_humidity(
            packet: api_m.RedvoxPacketM,
    ) -> Optional[SensorData]:
        """
        load relative humidity data from a single redvox packet

        :param packet: packet with data to load
        :return: relative humidity sensor data if it exists, None otherwise
        """
        return load_single(
            packet,
            SensorType.RELATIVE_HUMIDITY,
        )

    def load_apim_rel_humidity_from_list(
            packets: List[api_m.RedvoxPacketM], gaps: List[Tuple[float, float]]
    ) -> Optional[SensorData]:
        """
        load relative humidity data from a list of redvox packets

        :param packets: packets with data to load
        :param gaps: the list of non-inclusive start and end times of the gaps in the packets
        :return: relative humidity sensor data if it exists, None otherwise
        """
        return load_single_from_list(
            packets,
            gaps,
            SensorType.RELATIVE_HUMIDITY,
        )

    def load_xyz(
            packet: api_m.RedvoxPacketM,
            sensor_type: SensorType,
    ):
        field_name: str = __SENSOR_TYPE_TO_FIELD_NAME[sensor_type]
        sensor_fn: Optional[
            Callable[[api_m.RedvoxPacketM], Sensor]
        ] = __SENSOR_TYPE_TO_SENSOR_FN[sensor_type]
        if __has_sensor(packet, field_name) and sensor_fn is not None:
            sensor = sensor_fn(packet)
            data_df = read_apim_xyz_sensor(sensor, field_name)
            sample_rate, sample_interval, sample_interval_std = get_sample_statistics(
                data_df
            )
            return SensorData(
                sensor.sensor_description,
                data_df,
                sensor_type,
                sample_rate,
                sample_interval,
                sample_interval_std,
                False,
            )

    def load_xyz_from_list(
            packets: List[api_m.RedvoxPacketM],
            gaps: List[Tuple[float, float]],
            sensor_type: SensorType,
    ) -> Optional[SensorData]:
        field_name: str = __SENSOR_TYPE_TO_FIELD_NAME[sensor_type]
        data_list: List[List[float]] = [[], [], [], []]
        sensor_stats: StatsContainer = StatsContainer(f"{field_name}_sensor")
        packet: api_m.RedvoxPacketM
        sensor_fn: Optional[
            Callable[[api_m.RedvoxPacketM], Sensor]
        ] = __SENSOR_TYPE_TO_SENSOR_FN[sensor_type]
        for packet in packets:
            if __has_sensor(packet, field_name) and sensor_fn is not None:
                sensor: api_m.RedvoxPacketM.Sensors.Xyz = sensor_fn(packet)

                ts = sensor.timestamps.timestamps
                data_list[0].extend(ts)
                data_list[1].extend(sensor.x_samples.values)
                data_list[2].extend(sensor.y_samples.values)
                data_list[3].extend(sensor.z_samples.values)
                if len(sensor.timestamps.timestamps) == 1:
                    sensor_stats.add(__packet_duration_us(packet), 0, 1)
                else:
                    sensor_stats.add(np.mean(np.diff(ts)), np.std(np.diff(ts)), len(ts) - 1)
        if len(data_list[0]) > 0:
            return load_apim_xyz_sensor_from_list(
                sensor_type,
                data_list,
                gaps,
                field_name,
                get_sensor_description_list(packets, sensor_type),
                sensor_stats.mean_of_means(),
            )
        return None

    def load_apim_accelerometer(
            packet: api_m.RedvoxPacketM,
    ) -> Optional[SensorData]:
        """
        load accelerometer data from a single redvox packet

        :param packet: packet with data to load
        :return: accelerometer sensor data if it exists, None otherwise
        """
        return load_xyz(
            packet,
            SensorType.ACCELEROMETER,
        )

    def load_apim_accelerometer_from_list(
            packets: List[api_m.RedvoxPacketM], gaps: List[Tuple[float, float]]
    ) -> Optional[SensorData]:
        """
        load accelerometer data from a list of redvox packets

        :param packets: packets with data to load
        :param gaps: the list of non-inclusive start and end times of the gaps in the packets
        :return: accelerometer sensor data if it exists, None otherwise
        """
        return load_xyz_from_list(
            packets,
            gaps,
            SensorType.ACCELEROMETER,
        )

    def load_apim_magnetometer(
            packet: api_m.RedvoxPacketM,
    ) -> Optional[SensorData]:
        """
        load magnetometer data from a single redvox packet

        :param packet: packet with data to load
        :return: magnetometer sensor data if it exists, None otherwise
        """
        return load_xyz(
            packet,
            SensorType.MAGNETOMETER,
        )

    def load_apim_magnetometer_from_list(
            packets: List[api_m.RedvoxPacketM], gaps: List[Tuple[float, float]]
    ) -> Optional[SensorData]:
        """
        load magnetometer data from a list of redvox packets

        :param packets: packets with data to load
        :param gaps: the list of non-inclusive start and end times of the gaps in the packets
        :return: magnetometer sensor data if it exists, None otherwise
        """
        return load_xyz_from_list(
            packets,
            gaps,
            SensorType.MAGNETOMETER,
        )

    def load_apim_gyroscope(packet: api_m.RedvoxPacketM) -> Optional[SensorData]:
        """
        load gyroscope data from a single redvox packet

        :param packet: packet with data to load
        :return: gyroscope sensor data if it exists, None otherwise
        """
        return load_xyz(packet, SensorType.GYROSCOPE)

    def load_apim_gyroscope_from_list(
            packets: List[api_m.RedvoxPacketM], gaps: List[Tuple[float, float]]
    ) -> Optional[SensorData]:
        """
        load gyroscope data from a list of redvox packets

        :param packets: packets with data to load
        :param gaps: the list of non-inclusive start and end times of the gaps in the packets
        :return: gyroscope sensor data if it exists, None otherwise
        """
        return load_xyz_from_list(
            packets,
            gaps,
            SensorType.GYROSCOPE,
        )

    def load_apim_gravity(packet: api_m.RedvoxPacketM) -> Optional[SensorData]:
        """
        load gravity data from a single redvox packet

        :param packet: packet with data to load
        :return: gravity sensor data if it exists, None otherwise
        """
        return load_xyz(packet, SensorType.GRAVITY)

    def load_apim_gravity_from_list(
            packets: List[api_m.RedvoxPacketM], gaps: List[Tuple[float, float]]
    ) -> Optional[SensorData]:
        """
        load gravity data from a list of redvox packets

        :param packets: packets with data to load
        :param gaps: the list of non-inclusive start and end times of the gaps in the packets
        :return: gravity sensor data if it exists, None otherwise
        """
        return load_xyz_from_list(
            packets,
            gaps,
            SensorType.GRAVITY,
        )

    def load_apim_orientation(packet: api_m.RedvoxPacketM) -> Optional[SensorData]:
        """
        load orientation data from a single redvox packet

        :param packet: packet with data to load
        :return: orientation sensor data if it exists, None otherwise
        """
        return load_xyz(
            packet,
            SensorType.ORIENTATION,
        )

    def load_apim_orientation_from_list(
            packets: List[api_m.RedvoxPacketM], gaps: List[Tuple[float, float]]
    ) -> Optional[SensorData]:
        """
        load orientation data from a list of redvox packets

        :param packets: packets with data to load
        :param gaps: the list of non-inclusive start and end times of the gaps in the packets
        :return: orientation sensor data if it exists, None otherwise
        """
        return load_xyz_from_list(
            packets,
            gaps,
            SensorType.ORIENTATION,
        )

    def load_apim_linear_accel(
            packet: api_m.RedvoxPacketM,
    ) -> Optional[SensorData]:
        """
        load linear acceleration data from a single redvox packet

        :param packet: packet with data to load
        :return: linear acceleration sensor data if it exists, None otherwise
        """
        return load_xyz(
            packet,
            SensorType.LINEAR_ACCELERATION,
        )

    def load_apim_linear_accel_from_list(
            packets: List[api_m.RedvoxPacketM], gaps: List[Tuple[float, float]]
    ) -> Optional[SensorData]:
        """
        load linear acceleration data from a list of redvox packets

        :param packets: packets with data to load
        :param gaps: the list of non-inclusive start and end times of the gaps in the packets
        :return: linear acceleration sensor data if it exists, None otherwise
        """
        return load_xyz_from_list(
            packets,
            gaps,
            SensorType.LINEAR_ACCELERATION,
        )

    def load_apim_rotation_vector(
            packet: api_m.RedvoxPacketM,
    ) -> Optional[SensorData]:
        """
        load rotation vector data from a single redvox packet

        :param packet: packet with data to load
        :return: rotation vector sensor data if it exists, None otherwise
        """
        return load_xyz(
            packet,
            SensorType.ROTATION_VECTOR,
        )

    def load_apim_rotation_vector_from_list(
            packets: List[api_m.RedvoxPacketM], gaps: List[Tuple[float, float]]
    ) -> Optional[SensorData]:
        """
        load rotation vector data from a list of redvox packets

        :param packets: packets with data to load
        :param gaps: the list of non-inclusive start and end times of the gaps in the packets
        :return: rotation vector sensor data if it exists, None otherwise
        """
        return load_xyz_from_list(
            packets,
            gaps,
            SensorType.ROTATION_VECTOR,
        )

    def load_apim_health(packet: api_m.RedvoxPacketM) -> Optional[SensorData]:
        """
        load station health data from a single redvox packet

        :param packet: packet with data to load
        :return: station health data if it exists, None otherwise
        """
        metrics: api_m.RedvoxPacketM.StationInformation.StationMetrics = (
            packet.station_information.station_metrics
        )
        timestamps = metrics.timestamps.timestamps
        if len(timestamps) > 0:
            bat_samples = metrics.battery.values
            bat_cur_samples = metrics.battery_current.values
            temp_samples = metrics.temperature.values
            net_samples = metrics.network_type
            net_str_samples = metrics.network_strength.values
            pow_samples = metrics.power_state
            avail_ram_samples = metrics.available_ram.values
            avail_disk_samples = metrics.available_disk.values
            cell_samples = metrics.cell_service_state
            data_for_df = []
            for i in range(len(timestamps)):
                new_entry = [
                    timestamps[i],
                    timestamps[i],
                    np.nan if len(bat_samples) < i + 1 else bat_samples[i],
                    np.nan if len(bat_cur_samples) < i + 1 else bat_cur_samples[i],
                    np.nan if len(temp_samples) < i + 1 else temp_samples[i],
                    np.nan if len(net_samples) < i + 1 else net_samples[i],
                    np.nan if len(net_str_samples) < i + 1 else net_str_samples[i],
                    np.nan if len(pow_samples) < i + 1 else pow_samples[i],
                    np.nan if len(avail_ram_samples) < i + 1 else avail_ram_samples[i],
                    np.nan if len(avail_disk_samples) < i + 1 else avail_disk_samples[i],
                    np.nan if len(cell_samples) < i + 1 else cell_samples[i],
                ]
                data_for_df.append(new_entry)
            data_df = pd.DataFrame(
                data_for_df,
                columns=STATION_HEALTH_COLUMNS,
            )
            # health is collected 1 per packet or 1 per second
            sample_rate, sample_interval, sample_interval_std = __stats_for_sensor_per_packet_per_second(
                1, __packet_duration_s(packet), timestamps
            )
            return SensorData(
                "station health",
                data_df,
                SensorType.STATION_HEALTH,
                sample_rate,
                sample_interval,
                sample_interval_std,
                True
                )
        return None

    def load_apim_health_from_list(
            packets: List[api_m.RedvoxPacketM], gaps: List[Tuple[float, float]]
    ) -> Optional[SensorData]:
        """
        load station health data from a list of redvox packets

        :param packets: packets with data to load
        :param gaps: the list of non-inclusive start and end times of the gaps in the packets
        :return: station health sensor data if it exists, None otherwise
        """
        data_list: List[List[float]] = [[], [], [], [], [], [], [], [], [], []]
        for packet in packets:
            metrics = packet.station_information.station_metrics
            timestamps = metrics.timestamps.timestamps
            num_samples = len(timestamps)
            if num_samples > 0:
                data_list[0].extend(timestamps)
                samples = metrics.battery.values
                if len(samples) != num_samples:
                    samples = np.full(num_samples, np.nan)
                data_list[1].extend(samples)
                samples = metrics.battery_current.values
                if len(samples) != num_samples:
                    samples = np.full(num_samples, np.nan)
                data_list[2].extend(samples)
                samples = metrics.temperature.values
                if len(samples) != num_samples:
                    samples = np.full(num_samples, np.nan)
                data_list[3].extend(samples)
                samples = metrics.network_type
                data_list[4].extend(
                    [
                        NetworkType["UNKNOWN_NETWORK"].value
                        if len(samples) < i + 1
                        else samples[i]
                        for i in range(num_samples)
                    ]
                )
                samples = metrics.network_strength.values
                if len(samples) != num_samples:
                    samples = np.full(num_samples, np.nan)
                data_list[5].extend(samples)
                samples = metrics.power_state
                data_list[6].extend(
                    [
                        PowerState["UNKNOWN_POWER_STATE"].value
                        if len(samples) < i + 1
                        else samples[i]
                        for i in range(num_samples)
                    ]
                )
                samples = metrics.available_ram.values
                if len(samples) != num_samples:
                    samples = np.full(num_samples, np.nan)
                data_list[7].extend(samples)
                samples = metrics.available_disk.values
                data_list[8].extend(
                    samples if len(samples) == num_samples else np.full(num_samples, np.nan)
                )
                samples = metrics.cell_service_state
                data_list[9].extend(
                    [
                        CellServiceState["UNKNOWN"].value if len(samples) < i + 1 else samples[i]
                        for i in range(num_samples)
                    ]
                )
        if len(data_list[0]) > 0:
            data_list.insert(1, data_list[0].copy())
            # health is collected 1 per packet or 1 per second
            sample_rate, sample_interval, sample_interval_std = __stats_for_sensor_per_packet_per_second(
                len(packets), __packet_duration_s(packets[0]), data_list[0]
            )
            df = gpu.fill_gaps(
                pd.DataFrame(
                    np.transpose(data_list),
                    columns=STATION_HEALTH_COLUMNS,
                ),
                gaps,
                dtu.seconds_to_microseconds(sample_interval),
                True,
            )
            return SensorData(
                "station health",
                df,
                SensorType.STATION_HEALTH,
                sample_rate,
                sample_interval,
                sample_interval_std,
                True
            )
        return None

## Functions

`def get_empty_sensor_data(name: str, sensor_type: SensorType = SensorType.UNKNOWN_SENSOR) ‑> SensorData`

  create a sensor data object with no data

  :param name: name of the sensor :param sensor_type: type of the sensor to create, default SensorType.UNKNOWN_SENSOR :return: empty sensor

  Expand source code

      def get_empty_sensor_data(
              name: str, sensor_type: SensorType = SensorType.UNKNOWN_SENSOR
      ) -> SensorData:
          """
          create a sensor data object with no data

          :param name: name of the sensor
          :param sensor_type: type of the sensor to create, default SensorType.UNKNOWN_SENSOR
          :return: empty sensor
          """
          return SensorData(name, pd.DataFrame([], columns=["timestamps"]), sensor_type)

`def get_sample_statistics(data_df: pandas.core.frame.DataFrame) ‑> Tuple[float, float, float]`

  calculate the sample rate, interval and interval std dev using the timestamps in the dataframe

  :param data_df: the dataframe containing timestamps to calculate statistics from :return: a Tuple containing the sample rate, interval and interval std dev

  Expand source code

      def get_sample_statistics(data_df: pd.DataFrame) -> Tuple[float, float, float]:
          """
          calculate the sample rate, interval and interval std dev using the timestamps in the dataframe

          :param data_df: the dataframe containing timestamps to calculate statistics from
          :return: a Tuple containing the sample rate, interval and interval std dev
          """
          sample_interval: float
          sample_interval_std: float
          if data_df["timestamps"].size > 1:
              sample_interval = dtu.microseconds_to_seconds(
                  float(np.mean(np.diff(data_df["timestamps"])))
              )
              sample_interval_std = dtu.microseconds_to_seconds(
                  float(np.std(np.diff(data_df["timestamps"])))
              )
          else:
              sample_interval = np.nan
              sample_interval_std = np.nan
          return 1.0 / sample_interval, sample_interval, sample_interval_std

`def get_sensor_description(sensor: Union[src.redvox_api_m.redvox_api_m_pb2.Xyz, src.redvox_api_m.redvox_api_m_pb2.Single, src.redvox_api_m.redvox_api_m_pb2.Audio, src.redvox_api_m.redvox_api_m_pb2.Image, src.redvox_api_m.redvox_api_m_pb2.Location, src.redvox_api_m.redvox_api_m_pb2.CompressedAudio]) ‑> str`

  read the sensor's description from the sensor

  :param sensor: the sensor to read the description from :return: the sensor's description

  Expand source code

      def get_sensor_description(sensor: Sensor) -> str:
          """
          read the sensor's description from the sensor

          :param sensor: the sensor to read the description from
          :return: the sensor's description
          """
          return sensor.sensor_description

`def get_sensor_description_list(packets: List[src.redvox_api_m.redvox_api_m_pb2.RedvoxPacketM], sensor_type: SensorType) ‑> Optional[str]`

  read the sensor_type sensor's description from a list of packets

  :param packets: the list of packets to read from :param sensor_type: the SensorType of the sensor to read the description of :return: the sensor_type sensor's description

  Expand source code

      def get_sensor_description_list(
              packets: List[api_m.RedvoxPacketM], sensor_type: SensorType
      ) -> Optional[str]:
          """
          read the sensor_type sensor's description from a list of packets

          :param packets: the list of packets to read from
          :param sensor_type: the SensorType of the sensor to read the description of
          :return: the sensor_type sensor's description
          """

          field_name: str = __SENSOR_TYPE_TO_FIELD_NAME[sensor_type]
          sensor_fn: Optional[
              Callable[[api_m.RedvoxPacketM], Sensor]
          ] = __SENSOR_TYPE_TO_SENSOR_FN[sensor_type]
          for packet in packets:
              if __has_sensor(packet, field_name) and sensor_fn is not None:
                  return sensor_fn(packet).sensor_description

          return None

`def load_apim_accelerometer(packet: src.redvox_api_m.redvox_api_m_pb2.RedvoxPacketM) ‑> Optional[SensorData]`

  load accelerometer data from a single redvox packet

  :param packet: packet with data to load :return: accelerometer sensor data if it exists, None otherwise

  Expand source code

      def load_apim_accelerometer(
              packet: api_m.RedvoxPacketM,
      ) -> Optional[SensorData]:
          """
          load accelerometer data from a single redvox packet

          :param packet: packet with data to load
          :return: accelerometer sensor data if it exists, None otherwise
          """
          return load_xyz(
              packet,
              SensorType.ACCELEROMETER,
          )

`def load_apim_accelerometer_from_list(packets: List[src.redvox_api_m.redvox_api_m_pb2.RedvoxPacketM], gaps: List[Tuple[float, float]]) ‑> Optional[SensorData]`

  load accelerometer data from a list of redvox packets

  :param packets: packets with data to load :param gaps: the list of non-inclusive start and end times of the gaps in the packets :return: accelerometer sensor data if it exists, None otherwise

  Expand source code

      def load_apim_accelerometer_from_list(
              packets: List[api_m.RedvoxPacketM], gaps: List[Tuple[float, float]]
      ) -> Optional[SensorData]:
          """
          load accelerometer data from a list of redvox packets

          :param packets: packets with data to load
          :param gaps: the list of non-inclusive start and end times of the gaps in the packets
          :return: accelerometer sensor data if it exists, None otherwise
          """
          return load_xyz_from_list(
              packets,
              gaps,
              SensorType.ACCELEROMETER,
          )

`def load_apim_ambient_temp(packet: src.redvox_api_m.redvox_api_m_pb2.RedvoxPacketM) ‑> Optional[SensorData]`

  load ambient temperature data from a single redvox packet

  :param packet: packet with data to load :return: ambient temperature sensor data if it exists, None otherwise

  Expand source code

      def load_apim_ambient_temp(
              packet: api_m.RedvoxPacketM,
      ) -> Optional[SensorData]:
          """
          load ambient temperature data from a single redvox packet

          :param packet: packet with data to load
          :return: ambient temperature sensor data if it exists, None otherwise
          """
          return load_single(
              packet,
              SensorType.AMBIENT_TEMPERATURE,
          )

`def load_apim_ambient_temp_from_list(packets: List[src.redvox_api_m.redvox_api_m_pb2.RedvoxPacketM], gaps: List[Tuple[float, float]]) ‑> Optional[SensorData]`

  load ambient temperature data from a list of redvox packets

  :param packets: packets with data to load :param gaps: the list of non-inclusive start and end times of the gaps in the packets :return: ambient temperature sensor data if it exists, None otherwise

  Expand source code

      def load_apim_ambient_temp_from_list(
              packets: List[api_m.RedvoxPacketM], gaps: List[Tuple[float, float]]
      ) -> Optional[SensorData]:
          """
          load ambient temperature data from a list of redvox packets

          :param packets: packets with data to load
          :param gaps: the list of non-inclusive start and end times of the gaps in the packets
          :return: ambient temperature sensor data if it exists, None otherwise
          """
          return load_single_from_list(
              packets,
              gaps,
              SensorType.AMBIENT_TEMPERATURE,
          )

`def load_apim_audio(packet: src.redvox_api_m.redvox_api_m_pb2.RedvoxPacketM) ‑> Optional[SensorData]`

  load audio data from a single redvox packet

  :param packet: packet with data to load :return: audio sensor data if it exists, None otherwise

  Expand source code

      def load_apim_audio(packet: api_m.RedvoxPacketM) -> Optional[SensorData]:
          """
          load audio data from a single redvox packet

          :param packet: packet with data to load
          :return: audio sensor data if it exists, None otherwise
          """
          if __has_sensor(packet, __AUDIO_FIELD_NAME):
              audio_sensor: api_m.RedvoxPacketM.Sensors.Audio = packet.sensors.audio

              timestamps: np.ndarray = gpu.calc_evenly_sampled_timestamps(
                  audio_sensor.first_sample_timestamp,
                  len(audio_sensor.samples.values),
                  dtu.seconds_to_microseconds(1.0 / audio_sensor.sample_rate),
              )

              return SensorData(
                  audio_sensor.sensor_description,
                  pd.DataFrame(
                      np.transpose(
                          [timestamps, timestamps, np.array(audio_sensor.samples.values)]
                      ),
                      columns=gpu.AUDIO_DF_COLUMNS,
                  ),
                  SensorType.AUDIO,
                  audio_sensor.sample_rate,
                  1.0 / audio_sensor.sample_rate,
                  0.0,
                  True,
                  )

          return None

`def load_apim_audio_from_list(packets: List[src.redvox_api_m.redvox_api_m_pb2.RedvoxPacketM]) ‑> Tuple[Optional[SensorData], List[Tuple[float, float]]]`

  load audio data from a list of redvox packets NOTE: This only works because audio sensors in the list should all have the same number of data points.

  :param packets: packets with data to load :return: audio sensor data if it exists, None otherwise

  Expand source code

      def load_apim_audio_from_list(
              packets: List[api_m.RedvoxPacketM],
      ) -> Tuple[Optional[SensorData], List[Tuple[float, float]]]:
          """
          load audio data from a list of redvox packets
          NOTE: This only works because audio sensors in the list should all have the same number of data points.

          :param packets: packets with data to load
          :return: audio sensor data if it exists, None otherwise
          """
          if len(packets) > 0:
              if __has_sensor(
                      packets[0], __AUDIO_FIELD_NAME
              ):
                  sample_rate_hz: float = packets[0].sensors.audio.sample_rate
                  packet_info = [
                      (
                          p.sensors.audio.first_sample_timestamp,
                          np.array(p.sensors.audio.samples.values),
                      )
                      for p in packets
                  ]
                  gp_result = gpu.fill_audio_gaps(
                      packet_info, dtu.seconds_to_microseconds(1 / sample_rate_hz)
                  )
                  sensor_data = SensorData(
                      get_sensor_description_list(packets, SensorType.AUDIO),
                      gp_result.result_df,
                      SensorType.AUDIO,
                      sample_rate_hz,
                      1 / sample_rate_hz,
                      0.0,
                      True,
                      )
                  if len(gp_result.errors.get()) > 0:
                      sensor_data.errors.extend_error(gp_result.errors)

                  return (
                      sensor_data,
                      gp_result.gaps,
                  )
          return None, []

`def load_apim_best_location(packet: src.redvox_api_m.redvox_api_m_pb2.RedvoxPacketM) ‑> Optional[SensorData]`

  load best location data from a single redvox packet

  :param packet: packet with data to load :return: best location sensor data if it exists, None otherwise

  Expand source code

      def load_apim_best_location(packet: api_m.RedvoxPacketM) -> Optional[SensorData]:
          """
          load best location data from a single redvox packet

          :param packet: packet with data to load
          :return: best location sensor data if it exists, None otherwise
          """
          if __has_sensor(packet, __LOCATION_FIELD_NAME):
              loc: api_m.RedvoxPacketM.Sensors.Location = packet.sensors.location
              if loc.HasField("last_best_location") or loc.HasField("overall_best_location"):
                  best_loc: api_m.RedvoxPacketM.Sensors.Location.BestLocation
                  if loc.HasField("last_best_location"):
                      best_loc = loc.last_best_location
                  else:
                      best_loc = loc.overall_best_location
                  data_df = pd.DataFrame([
                      [
                          packet.timing_information.packet_start_mach_timestamp,
                          best_loc.latitude_longitude_timestamp.mach,
                          best_loc.latitude_longitude_timestamp.gps,
                          best_loc.latitude,
                          best_loc.longitude,
                          best_loc.altitude,
                          best_loc.speed,
                          best_loc.bearing,
                          best_loc.horizontal_accuracy,
                          best_loc.vertical_accuracy,
                          best_loc.speed_accuracy,
                          best_loc.bearing_accuracy,
                          best_loc.location_provider,
                      ]
                  ], columns=LOCATION_COLUMNS)
                  sample_rate = 1 / __packet_duration_s(packet)
                  return SensorData(
                      loc.sensor_description,
                      data_df,
                      SensorType.BEST_LOCATION,
                      sample_rate,
                      1 / sample_rate,
                      np.nan,
                      False,
                  )
          return None

`def load_apim_best_location_from_list(packets: List[src.redvox_api_m.redvox_api_m_pb2.RedvoxPacketM], gaps: List[Tuple[float, float]]) ‑> Optional[SensorData]`

  load best location data from a list of redvox packets

  :param packets: packets with data to load :param gaps: the list of non-inclusive start and end times of the gaps in the packets :return: best location sensor data if it exists, None otherwise

  Expand source code

      def load_apim_best_location_from_list(
              packets: List[api_m.RedvoxPacketM], gaps: List[Tuple[float, float]]
      ) -> Optional[SensorData]:
          """
          load best location data from a list of redvox packets

          :param packets: packets with data to load
          :param gaps: the list of non-inclusive start and end times of the gaps in the packets
          :return: best location sensor data if it exists, None otherwise
          """
          data_list: List[List[float]] = [[], [], [], [], [], [], [], [], [], [], [], [], []]
          loc_stats = StatsContainer("location_sensor")
          for packet in packets:
              if __has_sensor(packet, __LOCATION_FIELD_NAME):
                  loc: api_m.RedvoxPacketM.Sensors.Location = packet.sensors.location
                  if loc.HasField("last_best_location") or loc.HasField("overall_best_location"):
                      best_loc: api_m.RedvoxPacketM.Sensors.Location.BestLocation
                      if loc.HasField("last_best_location"):
                          best_loc = loc.last_best_location
                      else:
                          best_loc = loc.overall_best_location
                      data_list[0].append(packet.timing_information.packet_start_mach_timestamp)
                      data_list[1].append(best_loc.latitude_longitude_timestamp.mach)
                      data_list[2].append(best_loc.latitude_longitude_timestamp.gps)
                      data_list[3].append(best_loc.latitude)
                      data_list[4].append(best_loc.longitude)
                      data_list[5].append(best_loc.altitude)
                      data_list[6].append(best_loc.speed)
                      data_list[7].append(best_loc.bearing)
                      data_list[8].append(best_loc.horizontal_accuracy)
                      data_list[9].append(best_loc.vertical_accuracy)
                      data_list[10].append(best_loc.speed_accuracy)
                      data_list[11].append(best_loc.bearing_accuracy)
                      data_list[12].append(best_loc.location_provider)
                      loc_stats.add(__packet_duration_us(packet), 0, 1)
          if len(data_list[0]) > 0:
              return SensorData(
                  get_sensor_description_list(packets, SensorType.BEST_LOCATION),
                  gpu.fill_gaps(
                      pd.DataFrame(np.transpose(data_list), columns=LOCATION_COLUMNS),
                      gaps,
                      loc_stats.mean_of_means(),
                      True,
                  ),
                  SensorType.BEST_LOCATION,
                  calculate_stats=True,
              )

`def load_apim_compressed_audio(packet: src.redvox_api_m.redvox_api_m_pb2.RedvoxPacketM) ‑> Optional[SensorData]`

  load compressed audio data from a single redvox packet

  :param packet: packet with data to load :return: compressed audio sensor data if it exists, None otherwise

  Expand source code

      def load_apim_compressed_audio(packet: api_m.RedvoxPacketM) -> Optional[SensorData]:
          """
          load compressed audio data from a single redvox packet

          :param packet: packet with data to load
          :return: compressed audio sensor data if it exists, None otherwise
          """
          if __has_sensor(packet, __COMPRESSED_AUDIO_FIELD_NAME):
              comp_audio: api_m.RedvoxPacketM.Sensors.CompressedAudio = (
                  packet.sensors.compressed_audio
              )
              sample_rate_hz = comp_audio.sample_rate
              return SensorData(
                  comp_audio.sensor_description,
                  pd.DataFrame(
                      np.transpose(
                          [
                              comp_audio.first_sample_timestamp,
                              comp_audio.first_sample_timestamp,
                              np.array(list(comp_audio.audio_bytes)),
                              comp_audio.audio_codec,
                          ]
                      ),
                      columns=COMPRESSED_AUDIO_COLUMNS,
                  ),
                  SensorType.COMPRESSED_AUDIO,
                  sample_rate_hz,
                  1 / sample_rate_hz,
                  0.0,
                  True,
                  )
          return None

`def load_apim_compressed_audio_from_list(packets: List[src.redvox_api_m.redvox_api_m_pb2.RedvoxPacketM], gaps: List[Tuple[float, float]]) ‑> Optional[SensorData]`

  load compressed audio data from a list of redvox packets

  :param packets: packets with data to load :param gaps: the list of non-inclusive start and end times of the gaps in the packets :return: compressed audio sensor data if it exists, None otherwise

  Expand source code

      def load_apim_compressed_audio_from_list(
              packets: List[api_m.RedvoxPacketM], gaps: List[Tuple[float, float]]
      ) -> Optional[SensorData]:
          """
          load compressed audio data from a list of redvox packets

          :param packets: packets with data to load
          :param gaps: the list of non-inclusive start and end times of the gaps in the packets
          :return: compressed audio sensor data if it exists, None otherwise
          """
          data_list = [[], [], []]
          for packet in packets:
              if __has_sensor(packet, __COMPRESSED_AUDIO_FIELD_NAME):
                  comp_audio = packet.sensors.compressed_audio
                  data_list[0].append(comp_audio.first_sample_timestamp)
                  data_list[1].append(comp_audio.audio_bytes)
                  data_list[2].append(comp_audio.audio_codec)

          if len(data_list[0]) > 0:
              data_df = gpu.fill_gaps(
                  pd.DataFrame(
                      np.transpose([data_list[0], data_list[0], data_list[1], data_list[2]]),
                      columns=COMPRESSED_AUDIO_COLUMNS,
                  ),
                  gaps,
                  __packet_duration_us(packets[0]),
                  True,
              )
              data_df["audio_codec"] = [d for d in data_df["audio_codec"]]
              sample_rate_hz = packets[0].sensors.compressed_audio.sample_rate
              return SensorData(
                  get_sensor_description_list(packets, SensorType.COMPRESSED_AUDIO),
                  data_df,
                  SensorType.COMPRESSED_AUDIO,
                  sample_rate_hz,
                  1 / sample_rate_hz,
                  0.0,
                  True,
                  )
          return None

`def load_apim_gravity(packet: src.redvox_api_m.redvox_api_m_pb2.RedvoxPacketM) ‑> Optional[SensorData]`

  load gravity data from a single redvox packet

  :param packet: packet with data to load :return: gravity sensor data if it exists, None otherwise

  Expand source code

      def load_apim_gravity(packet: api_m.RedvoxPacketM) -> Optional[SensorData]:
          """
          load gravity data from a single redvox packet

          :param packet: packet with data to load
          :return: gravity sensor data if it exists, None otherwise
          """
          return load_xyz(packet, SensorType.GRAVITY)

`def load_apim_gravity_from_list(packets: List[src.redvox_api_m.redvox_api_m_pb2.RedvoxPacketM], gaps: List[Tuple[float, float]]) ‑> Optional[SensorData]`

  load gravity data from a list of redvox packets

  :param packets: packets with data to load :param gaps: the list of non-inclusive start and end times of the gaps in the packets :return: gravity sensor data if it exists, None otherwise

  Expand source code

      def load_apim_gravity_from_list(
              packets: List[api_m.RedvoxPacketM], gaps: List[Tuple[float, float]]
      ) -> Optional[SensorData]:
          """
          load gravity data from a list of redvox packets

          :param packets: packets with data to load
          :param gaps: the list of non-inclusive start and end times of the gaps in the packets
          :return: gravity sensor data if it exists, None otherwise
          """
          return load_xyz_from_list(
              packets,
              gaps,
              SensorType.GRAVITY,
          )

`def load_apim_gyroscope(packet: src.redvox_api_m.redvox_api_m_pb2.RedvoxPacketM) ‑> Optional[SensorData]`

  load gyroscope data from a single redvox packet

  :param packet: packet with data to load :return: gyroscope sensor data if it exists, None otherwise

  Expand source code

      def load_apim_gyroscope(packet: api_m.RedvoxPacketM) -> Optional[SensorData]:
          """
          load gyroscope data from a single redvox packet

          :param packet: packet with data to load
          :return: gyroscope sensor data if it exists, None otherwise
          """
          return load_xyz(packet, SensorType.GYROSCOPE)

`def load_apim_gyroscope_from_list(packets: List[src.redvox_api_m.redvox_api_m_pb2.RedvoxPacketM], gaps: List[Tuple[float, float]]) ‑> Optional[SensorData]`

  load gyroscope data from a list of redvox packets

  :param packets: packets with data to load :param gaps: the list of non-inclusive start and end times of the gaps in the packets :return: gyroscope sensor data if it exists, None otherwise

  Expand source code

      def load_apim_gyroscope_from_list(
              packets: List[api_m.RedvoxPacketM], gaps: List[Tuple[float, float]]
      ) -> Optional[SensorData]:
          """
          load gyroscope data from a list of redvox packets

          :param packets: packets with data to load
          :param gaps: the list of non-inclusive start and end times of the gaps in the packets
          :return: gyroscope sensor data if it exists, None otherwise
          """
          return load_xyz_from_list(
              packets,
              gaps,
              SensorType.GYROSCOPE,
          )

`def load_apim_health(packet: src.redvox_api_m.redvox_api_m_pb2.RedvoxPacketM) ‑> Optional[SensorData]`

  load station health data from a single redvox packet

  :param packet: packet with data to load :return: station health data if it exists, None otherwise

  Expand source code

      def load_apim_health(packet: api_m.RedvoxPacketM) -> Optional[SensorData]:
          """
          load station health data from a single redvox packet

          :param packet: packet with data to load
          :return: station health data if it exists, None otherwise
          """
          metrics: api_m.RedvoxPacketM.StationInformation.StationMetrics = (
              packet.station_information.station_metrics
          )
          timestamps = metrics.timestamps.timestamps
          if len(timestamps) > 0:
              bat_samples = metrics.battery.values
              bat_cur_samples = metrics.battery_current.values
              temp_samples = metrics.temperature.values
              net_samples = metrics.network_type
              net_str_samples = metrics.network_strength.values
              pow_samples = metrics.power_state
              avail_ram_samples = metrics.available_ram.values
              avail_disk_samples = metrics.available_disk.values
              cell_samples = metrics.cell_service_state
              data_for_df = []
              for i in range(len(timestamps)):
                  new_entry = [
                      timestamps[i],
                      timestamps[i],
                      np.nan if len(bat_samples) < i + 1 else bat_samples[i],
                      np.nan if len(bat_cur_samples) < i + 1 else bat_cur_samples[i],
                      np.nan if len(temp_samples) < i + 1 else temp_samples[i],
                      np.nan if len(net_samples) < i + 1 else net_samples[i],
                      np.nan if len(net_str_samples) < i + 1 else net_str_samples[i],
                      np.nan if len(pow_samples) < i + 1 else pow_samples[i],
                      np.nan if len(avail_ram_samples) < i + 1 else avail_ram_samples[i],
                      np.nan if len(avail_disk_samples) < i + 1 else avail_disk_samples[i],
                      np.nan if len(cell_samples) < i + 1 else cell_samples[i],
                  ]
                  data_for_df.append(new_entry)
              data_df = pd.DataFrame(
                  data_for_df,
                  columns=STATION_HEALTH_COLUMNS,
              )
              # health is collected 1 per packet or 1 per second
              sample_rate, sample_interval, sample_interval_std = __stats_for_sensor_per_packet_per_second(
                  1, __packet_duration_s(packet), timestamps
              )
              return SensorData(
                  "station health",
                  data_df,
                  SensorType.STATION_HEALTH,
                  sample_rate,
                  sample_interval,
                  sample_interval_std,
                  True
                  )
          return None

`def load_apim_health_from_list(packets: List[src.redvox_api_m.redvox_api_m_pb2.RedvoxPacketM], gaps: List[Tuple[float, float]]) ‑> Optional[SensorData]`

  load station health data from a list of redvox packets

  :param packets: packets with data to load :param gaps: the list of non-inclusive start and end times of the gaps in the packets :return: station health sensor data if it exists, None otherwise

  Expand source code

      def load_apim_health_from_list(
              packets: List[api_m.RedvoxPacketM], gaps: List[Tuple[float, float]]
      ) -> Optional[SensorData]:
          """
          load station health data from a list of redvox packets

          :param packets: packets with data to load
          :param gaps: the list of non-inclusive start and end times of the gaps in the packets
          :return: station health sensor data if it exists, None otherwise
          """
          data_list: List[List[float]] = [[], [], [], [], [], [], [], [], [], []]
          for packet in packets:
              metrics = packet.station_information.station_metrics
              timestamps = metrics.timestamps.timestamps
              num_samples = len(timestamps)
              if num_samples > 0:
                  data_list[0].extend(timestamps)
                  samples = metrics.battery.values
                  if len(samples) != num_samples:
                      samples = np.full(num_samples, np.nan)
                  data_list[1].extend(samples)
                  samples = metrics.battery_current.values
                  if len(samples) != num_samples:
                      samples = np.full(num_samples, np.nan)
                  data_list[2].extend(samples)
                  samples = metrics.temperature.values
                  if len(samples) != num_samples:
                      samples = np.full(num_samples, np.nan)
                  data_list[3].extend(samples)
                  samples = metrics.network_type
                  data_list[4].extend(
                      [
                          NetworkType["UNKNOWN_NETWORK"].value
                          if len(samples) < i + 1
                          else samples[i]
                          for i in range(num_samples)
                      ]
                  )
                  samples = metrics.network_strength.values
                  if len(samples) != num_samples:
                      samples = np.full(num_samples, np.nan)
                  data_list[5].extend(samples)
                  samples = metrics.power_state
                  data_list[6].extend(
                      [
                          PowerState["UNKNOWN_POWER_STATE"].value
                          if len(samples) < i + 1
                          else samples[i]
                          for i in range(num_samples)
                      ]
                  )
                  samples = metrics.available_ram.values
                  if len(samples) != num_samples:
                      samples = np.full(num_samples, np.nan)
                  data_list[7].extend(samples)
                  samples = metrics.available_disk.values
                  data_list[8].extend(
                      samples if len(samples) == num_samples else np.full(num_samples, np.nan)
                  )
                  samples = metrics.cell_service_state
                  data_list[9].extend(
                      [
                          CellServiceState["UNKNOWN"].value if len(samples) < i + 1 else samples[i]
                          for i in range(num_samples)
                      ]
                  )
          if len(data_list[0]) > 0:
              data_list.insert(1, data_list[0].copy())
              # health is collected 1 per packet or 1 per second
              sample_rate, sample_interval, sample_interval_std = __stats_for_sensor_per_packet_per_second(
                  len(packets), __packet_duration_s(packets[0]), data_list[0]
              )
              df = gpu.fill_gaps(
                  pd.DataFrame(
                      np.transpose(data_list),
                      columns=STATION_HEALTH_COLUMNS,
                  ),
                  gaps,
                  dtu.seconds_to_microseconds(sample_interval),
                  True,
              )
              return SensorData(
                  "station health",
                  df,
                  SensorType.STATION_HEALTH,
                  sample_rate,
                  sample_interval,
                  sample_interval_std,
                  True
              )
          return None

`def load_apim_image(packet: src.redvox_api_m.redvox_api_m_pb2.RedvoxPacketM) ‑> Optional[SensorData]`

  load image data from a single redvox packet

  :param packet: packet with data to load :return: image sensor data if it exists, None otherwise

  Expand source code

      def load_apim_image(packet: api_m.RedvoxPacketM) -> Optional[SensorData]:
          """
          load image data from a single redvox packet

          :param packet: packet with data to load
          :return: image sensor data if it exists, None otherwise
          """
          if __has_sensor(packet, __IMAGE_FIELD_NAME):
              image_sensor: api_m.RedvoxPacketM.Sensors.Image = packet.sensors.image
              timestamps = image_sensor.timestamps.timestamps
              codecs = np.full(len(timestamps), image_sensor.image_codec)
              data_df = pd.DataFrame(
                  np.transpose([timestamps, timestamps, image_sensor.samples, codecs]),
                  columns=IMAGE_COLUMNS,
              )
              data_df["image_codec"] = [d for d in data_df["image_codec"]]
              # image is collected 1 per packet or 1 per second
              sample_rate, sample_interval, sample_interval_std = __stats_for_sensor_per_packet_per_second(
                  1, __packet_duration_s(packet), timestamps
              )
              return SensorData(
                  image_sensor.sensor_description,
                  data_df,
                  SensorType.IMAGE,
                  sample_rate,
                  sample_interval,
                  sample_interval_std,
                  True,
              )
          return None

`def load_apim_image_from_list(packets: List[src.redvox_api_m.redvox_api_m_pb2.RedvoxPacketM], gaps: List[Tuple[float, float]]) ‑> Optional[SensorData]`

  load image data from a list of redvox packets

  :param packets: packets with data to load :param gaps: the list of non-inclusive start and end times of the gaps in the packets :return: image sensor data if it exists, None otherwise

  Expand source code

      def load_apim_image_from_list(
              packets: List[api_m.RedvoxPacketM], gaps: List[Tuple[float, float]]
      ) -> Optional[SensorData]:
          """
          load image data from a list of redvox packets

          :param packets: packets with data to load
          :param gaps: the list of non-inclusive start and end times of the gaps in the packets
          :return: image sensor data if it exists, None otherwise
          """
          data_list: List[List[float]] = [[], [], []]
          for packet in packets:
              if __has_sensor(packet, __IMAGE_FIELD_NAME):
                  image_sensor = packet.sensors.image

                  data_list[0].extend(image_sensor.timestamps.timestamps)
                  data_list[1].extend(image_sensor.samples)
                  data_list[2].extend(
                      [
                          image_sensor.image_codec
                          for i in range(len(image_sensor.timestamps.timestamps))
                      ]
                  )
          if len(data_list[0]) > 0:
              # image is collected 1 per packet or 1 per second
              sample_rate, sample_interval, sample_interval_std = __stats_for_sensor_per_packet_per_second(
                  len(packets), __packet_duration_s(packets[0]), data_list[0]
              )
              df = pd.DataFrame(
                  np.transpose(
                      [data_list[0], data_list[0], data_list[1], data_list[2]]
                  ),
                  columns=IMAGE_COLUMNS,
              )
              df["timestamps"] = df["timestamps"].astype(float)
              df["unaltered_timestamps"] = df["unaltered_timestamps"].astype(float)
              df["image_codec"] = df["image_codec"].astype(float)
              return SensorData(
                  get_sensor_description_list(packets, SensorType.IMAGE),
                  gpu.fill_gaps(
                      df,
                      gaps,
                      dtu.seconds_to_microseconds(sample_interval),
                      True,
                  ),
                  SensorType.IMAGE,
                  sample_rate,
                  sample_interval,
                  sample_interval_std,
                  True,
              )
          return None

`def load_apim_light(packet: src.redvox_api_m.redvox_api_m_pb2.RedvoxPacketM) ‑> Optional[SensorData]`

  load light data from a single redvox packet

  :param packet: packet with data to load :return: light sensor data if it exists, None otherwise

  Expand source code

      def load_apim_light(packet: api_m.RedvoxPacketM) -> Optional[SensorData]:
          """
          load light data from a single redvox packet

          :param packet: packet with data to load
          :return: light sensor data if it exists, None otherwise
          """
          return load_single(packet, SensorType.LIGHT)

`def load_apim_light_from_list(packets: List[src.redvox_api_m.redvox_api_m_pb2.RedvoxPacketM], gaps: List[Tuple[float, float]]) ‑> Optional[SensorData]`

  load light data from a list of redvox packets

  :param packets: packets with data to load :param gaps: the list of non-inclusive start and end times of the gaps in the packets :return: light sensor data if it exists, None otherwise

  Expand source code

      def load_apim_light_from_list(
              packets: List[api_m.RedvoxPacketM], gaps: List[Tuple[float, float]]
      ) -> Optional[SensorData]:
          """
          load light data from a list of redvox packets

          :param packets: packets with data to load
          :param gaps: the list of non-inclusive start and end times of the gaps in the packets
          :return: light sensor data if it exists, None otherwise
          """
          return load_single_from_list(
              packets,
              gaps,
              SensorType.LIGHT,
          )

`def load_apim_linear_accel(packet: src.redvox_api_m.redvox_api_m_pb2.RedvoxPacketM) ‑> Optional[SensorData]`

  load linear acceleration data from a single redvox packet

  :param packet: packet with data to load :return: linear acceleration sensor data if it exists, None otherwise

  Expand source code

      def load_apim_linear_accel(
              packet: api_m.RedvoxPacketM,
      ) -> Optional[SensorData]:
          """
          load linear acceleration data from a single redvox packet

          :param packet: packet with data to load
          :return: linear acceleration sensor data if it exists, None otherwise
          """
          return load_xyz(
              packet,
              SensorType.LINEAR_ACCELERATION,
          )

`def load_apim_linear_accel_from_list(packets: List[src.redvox_api_m.redvox_api_m_pb2.RedvoxPacketM], gaps: List[Tuple[float, float]]) ‑> Optional[SensorData]`

  load linear acceleration data from a list of redvox packets

  :param packets: packets with data to load :param gaps: the list of non-inclusive start and end times of the gaps in the packets :return: linear acceleration sensor data if it exists, None otherwise

  Expand source code

      def load_apim_linear_accel_from_list(
              packets: List[api_m.RedvoxPacketM], gaps: List[Tuple[float, float]]
      ) -> Optional[SensorData]:
          """
          load linear acceleration data from a list of redvox packets

          :param packets: packets with data to load
          :param gaps: the list of non-inclusive start and end times of the gaps in the packets
          :return: linear acceleration sensor data if it exists, None otherwise
          """
          return load_xyz_from_list(
              packets,
              gaps,
              SensorType.LINEAR_ACCELERATION,
          )

`def load_apim_location(packet: src.redvox_api_m.redvox_api_m_pb2.RedvoxPacketM) ‑> Optional[SensorData]`

  load location data from a single packet

  :param packet: packet with data to load :return: location sensor data if it exists, None otherwise

  Expand source code

      def load_apim_location(packet: api_m.RedvoxPacketM) -> Optional[SensorData]:
          """
          load location data from a single packet

          :param packet: packet with data to load
          :return: location sensor data if it exists, None otherwise
          """
          if __has_sensor(packet, __LOCATION_FIELD_NAME):
              loc: api_m.RedvoxPacketM.Sensors.Location = packet.sensors.location
              timestamps = loc.timestamps.timestamps
              if len(timestamps) > 0:
                  gps_timestamps = loc.timestamps_gps.timestamps
                  lat_samples = loc.latitude_samples.values
                  lon_samples = loc.longitude_samples.values
                  alt_samples = loc.altitude_samples.values
                  spd_samples = loc.speed_samples.values
                  bear_samples = loc.bearing_samples.values
                  hor_acc_samples = loc.horizontal_accuracy_samples.values
                  vert_acc_samples = loc.vertical_accuracy_samples.values
                  spd_acc_samples = loc.speed_accuracy_samples.values
                  bear_acc_samples = loc.bearing_accuracy_samples.values
                  loc_prov_samples = loc.location_providers
                  data_for_df = []
                  for i in range(len(timestamps)):
                      new_entry = [
                          timestamps[i],
                          timestamps[i],
                          np.nan if len(gps_timestamps) <= i else gps_timestamps[i],
                          lat_samples[i],
                          lon_samples[i],
                          np.nan if len(alt_samples) <= i else alt_samples[i],
                          np.nan if len(spd_samples) <= i else spd_samples[i],
                          np.nan if len(bear_samples) <= i else bear_samples[i],
                          np.nan if len(hor_acc_samples) <= i else hor_acc_samples[i],
                          np.nan if len(vert_acc_samples) <= i else vert_acc_samples[i],
                          np.nan if len(spd_acc_samples) <= i else spd_acc_samples[i],
                          np.nan if len(bear_acc_samples) <= i else bear_acc_samples[i],
                          np.nan if len(loc_prov_samples) <= i else loc_prov_samples[i],
                      ]
                      data_for_df.append(new_entry)
              else:
                  return None
              data_df = pd.DataFrame(data_for_df, columns=LOCATION_COLUMNS)
              sample_rate, sample_interval, sample_interval_std = get_sample_statistics(
                  data_df
              )
              return SensorData(
                  loc.sensor_description,
                  data_df,
                  SensorType.LOCATION,
                  sample_rate,
                  sample_interval,
                  sample_interval_std,
                  False,
              )
          return None

`def load_apim_location_from_list(packets: List[src.redvox_api_m.redvox_api_m_pb2.RedvoxPacketM], gaps: List[Tuple[float, float]]) ‑> Optional[SensorData]`

  load location data from a list of redvox packets

  :param packets: packets with data to load :param gaps: the list of non-inclusive start and end times of the gaps in the packets :return: location sensor data if it exists, None otherwise

  Expand source code

      def load_apim_location_from_list(
              packets: List[api_m.RedvoxPacketM], gaps: List[Tuple[float, float]]
      ) -> Optional[SensorData]:
          """
          load location data from a list of redvox packets

          :param packets: packets with data to load
          :param gaps: the list of non-inclusive start and end times of the gaps in the packets
          :return: location sensor data if it exists, None otherwise
          """
          data_list: List[List[float]] = [[], [], [], [], [], [], [], [], [], [], [], []]
          loc_stats = StatsContainer("location_sensor")
          for packet in packets:
              if __has_sensor(packet, __LOCATION_FIELD_NAME):
                  loc: api_m.RedvoxPacketM.Sensors.Location = packet.sensors.location

                  num_samples = len(loc.timestamps.timestamps)
                  if num_samples > 0:
                      samples = loc.timestamps.timestamps
                      data_list[0].extend(samples)
                      if num_samples == 1:
                          loc_stats.add(
                              __packet_duration_us(packet),
                              0,
                              1,
                          )
                      else:
                          loc_stats.add(
                              np.mean(np.diff(samples)),
                              np.std(np.diff(samples)),
                              num_samples - 1,
                              )
                      for i in range(num_samples):
                          samples = loc.timestamps_gps.timestamps
                          data_list[1].append(np.nan if len(samples) <= i else samples[i])
                          samples = loc.latitude_samples.values
                          data_list[2].append(samples[i])
                          samples = loc.longitude_samples.values
                          data_list[3].append(samples[i])
                          samples = loc.altitude_samples.values
                          data_list[4].append(
                              np.nan if len(samples) < i + 1 else samples[i]
                          )
                          samples = loc.speed_samples.values
                          data_list[5].append(
                              np.nan if len(samples) < i + 1 else samples[i]
                          )
                          samples = loc.bearing_samples.values
                          data_list[6].append(
                              np.nan if len(samples) < i + 1 else samples[i]
                          )
                          samples = loc.horizontal_accuracy_samples.values
                          data_list[7].append(
                              np.nan if len(samples) < i + 1 else samples[i]
                          )
                          samples = loc.vertical_accuracy_samples.values
                          data_list[8].append(
                              np.nan if len(samples) < i + 1 else samples[i]
                          )
                          samples = loc.speed_accuracy_samples.values
                          data_list[9].append(
                              np.nan if len(samples) < i + 1 else samples[i]
                          )
                          samples = loc.bearing_accuracy_samples.values
                          data_list[10].append(
                              np.nan if len(samples) < i + 1 else samples[i]
                          )
                          samples = list(loc.location_providers)
                          data_list[11].append(
                              api_m.RedvoxPacketM.Sensors.Location.LocationProvider.UNKNOWN
                              if len(samples) < i + 1
                              else samples[i]
                          )
          if len(data_list[0]) > 0:
              data_list.insert(1, data_list[0].copy())
              return SensorData(
                  get_sensor_description_list(packets, SensorType.LOCATION),
                  gpu.fill_gaps(
                      pd.DataFrame(np.transpose(data_list), columns=LOCATION_COLUMNS),
                      gaps,
                      loc_stats.mean_of_means(),
                      True,
                  ),
                  SensorType.LOCATION,
                  calculate_stats=True,
              )
          return None

`def load_apim_magnetometer(packet: src.redvox_api_m.redvox_api_m_pb2.RedvoxPacketM) ‑> Optional[SensorData]`

  load magnetometer data from a single redvox packet

  :param packet: packet with data to load :return: magnetometer sensor data if it exists, None otherwise

  Expand source code

      def load_apim_magnetometer(
              packet: api_m.RedvoxPacketM,
      ) -> Optional[SensorData]:
          """
          load magnetometer data from a single redvox packet

          :param packet: packet with data to load
          :return: magnetometer sensor data if it exists, None otherwise
          """
          return load_xyz(
              packet,
              SensorType.MAGNETOMETER,
          )

`def load_apim_magnetometer_from_list(packets: List[src.redvox_api_m.redvox_api_m_pb2.RedvoxPacketM], gaps: List[Tuple[float, float]]) ‑> Optional[SensorData]`

  load magnetometer data from a list of redvox packets

  :param packets: packets with data to load :param gaps: the list of non-inclusive start and end times of the gaps in the packets :return: magnetometer sensor data if it exists, None otherwise

  Expand source code

      def load_apim_magnetometer_from_list(
              packets: List[api_m.RedvoxPacketM], gaps: List[Tuple[float, float]]
      ) -> Optional[SensorData]:
          """
          load magnetometer data from a list of redvox packets

          :param packets: packets with data to load
          :param gaps: the list of non-inclusive start and end times of the gaps in the packets
          :return: magnetometer sensor data if it exists, None otherwise
          """
          return load_xyz_from_list(
              packets,
              gaps,
              SensorType.MAGNETOMETER,
          )

`def load_apim_orientation(packet: src.redvox_api_m.redvox_api_m_pb2.RedvoxPacketM) ‑> Optional[SensorData]`

  load orientation data from a single redvox packet

  :param packet: packet with data to load :return: orientation sensor data if it exists, None otherwise

  Expand source code

      def load_apim_orientation(packet: api_m.RedvoxPacketM) -> Optional[SensorData]:
          """
          load orientation data from a single redvox packet

          :param packet: packet with data to load
          :return: orientation sensor data if it exists, None otherwise
          """
          return load_xyz(
              packet,
              SensorType.ORIENTATION,
          )

`def load_apim_orientation_from_list(packets: List[src.redvox_api_m.redvox_api_m_pb2.RedvoxPacketM], gaps: List[Tuple[float, float]]) ‑> Optional[SensorData]`

  load orientation data from a list of redvox packets

  :param packets: packets with data to load :param gaps: the list of non-inclusive start and end times of the gaps in the packets :return: orientation sensor data if it exists, None otherwise

  Expand source code

      def load_apim_orientation_from_list(
              packets: List[api_m.RedvoxPacketM], gaps: List[Tuple[float, float]]
      ) -> Optional[SensorData]:
          """
          load orientation data from a list of redvox packets

          :param packets: packets with data to load
          :param gaps: the list of non-inclusive start and end times of the gaps in the packets
          :return: orientation sensor data if it exists, None otherwise
          """
          return load_xyz_from_list(
              packets,
              gaps,
              SensorType.ORIENTATION,
          )

`def load_apim_pressure(packet: src.redvox_api_m.redvox_api_m_pb2.RedvoxPacketM) ‑> Optional[SensorData]`

  load pressure data from a single redvox packet

  :param packet: packet with data to load :return: pressure sensor data if it exists, None otherwise

  Expand source code

      def load_apim_pressure(packet: api_m.RedvoxPacketM) -> Optional[SensorData]:
          """
          load pressure data from a single redvox packet

          :param packet: packet with data to load
          :return: pressure sensor data if it exists, None otherwise
          """
          return load_single(packet, SensorType.PRESSURE)

`def load_apim_pressure_from_list(packets: List[src.redvox_api_m.redvox_api_m_pb2.RedvoxPacketM], gaps: List[Tuple[float, float]]) ‑> Optional[SensorData]`

  load pressure data from a list of redvox packets

  :param packets: packets with data to load :param gaps: the list of non-inclusive start and end times of the gaps in the packets :return: pressure sensor data if it exists, None otherwise

  Expand source code

      def load_apim_pressure_from_list(
              packets: List[api_m.RedvoxPacketM], gaps: List[Tuple[float, float]]
      ) -> Optional[SensorData]:
          """
          load pressure data from a list of redvox packets

          :param packets: packets with data to load
          :param gaps: the list of non-inclusive start and end times of the gaps in the packets
          :return: pressure sensor data if it exists, None otherwise
          """
          return load_single_from_list(
              packets,
              gaps,
              SensorType.PRESSURE,
          )

`def load_apim_proximity(packet: src.redvox_api_m.redvox_api_m_pb2.RedvoxPacketM) ‑> Optional[SensorData]`

  load proximity data from a single redvox packet

  :param packet: packet with data to load :return: proximity sensor data if it exists, None otherwise

  Expand source code

      def load_apim_proximity(packet: api_m.RedvoxPacketM) -> Optional[SensorData]:
          """
          load proximity data from a single redvox packet

          :param packet: packet with data to load
          :return: proximity sensor data if it exists, None otherwise
          """
          return load_single(packet, SensorType.PROXIMITY)

`def load_apim_proximity_from_list(packets: List[src.redvox_api_m.redvox_api_m_pb2.RedvoxPacketM], gaps: List[Tuple[float, float]]) ‑> Optional[SensorData]`

  load proximity data from a list of redvox packets

  :param packets: packets with data to load :param gaps: the list of non-inclusive start and end times of the gaps in the packets :return: proximity sensor data if it exists, None otherwise

  Expand source code

      def load_apim_proximity_from_list(
              packets: List[api_m.RedvoxPacketM], gaps: List[Tuple[float, float]]
      ) -> Optional[SensorData]:
          """
          load proximity data from a list of redvox packets

          :param packets: packets with data to load
          :param gaps: the list of non-inclusive start and end times of the gaps in the packets
          :return: proximity sensor data if it exists, None otherwise
          """
          return load_single_from_list(
              packets,
              gaps,
              SensorType.PROXIMITY,
          )

`def load_apim_rel_humidity(packet: src.redvox_api_m.redvox_api_m_pb2.RedvoxPacketM) ‑> Optional[SensorData]`

  load relative humidity data from a single redvox packet

  :param packet: packet with data to load :return: relative humidity sensor data if it exists, None otherwise

  Expand source code

      def load_apim_rel_humidity(
              packet: api_m.RedvoxPacketM,
      ) -> Optional[SensorData]:
          """
          load relative humidity data from a single redvox packet

          :param packet: packet with data to load
          :return: relative humidity sensor data if it exists, None otherwise
          """
          return load_single(
              packet,
              SensorType.RELATIVE_HUMIDITY,
          )

`def load_apim_rel_humidity_from_list(packets: List[src.redvox_api_m.redvox_api_m_pb2.RedvoxPacketM], gaps: List[Tuple[float, float]]) ‑> Optional[SensorData]`

  load relative humidity data from a list of redvox packets

  :param packets: packets with data to load :param gaps: the list of non-inclusive start and end times of the gaps in the packets :return: relative humidity sensor data if it exists, None otherwise

  Expand source code

      def load_apim_rel_humidity_from_list(
              packets: List[api_m.RedvoxPacketM], gaps: List[Tuple[float, float]]
      ) -> Optional[SensorData]:
          """
          load relative humidity data from a list of redvox packets

          :param packets: packets with data to load
          :param gaps: the list of non-inclusive start and end times of the gaps in the packets
          :return: relative humidity sensor data if it exists, None otherwise
          """
          return load_single_from_list(
              packets,
              gaps,
              SensorType.RELATIVE_HUMIDITY,
          )

`def load_apim_rotation_vector(packet: src.redvox_api_m.redvox_api_m_pb2.RedvoxPacketM) ‑> Optional[SensorData]`

  load rotation vector data from a single redvox packet

  :param packet: packet with data to load :return: rotation vector sensor data if it exists, None otherwise

  Expand source code

      def load_apim_rotation_vector(
              packet: api_m.RedvoxPacketM,
      ) -> Optional[SensorData]:
          """
          load rotation vector data from a single redvox packet

          :param packet: packet with data to load
          :return: rotation vector sensor data if it exists, None otherwise
          """
          return load_xyz(
              packet,
              SensorType.ROTATION_VECTOR,
          )

`def load_apim_rotation_vector_from_list(packets: List[src.redvox_api_m.redvox_api_m_pb2.RedvoxPacketM], gaps: List[Tuple[float, float]]) ‑> Optional[SensorData]`

  load rotation vector data from a list of redvox packets

  :param packets: packets with data to load :param gaps: the list of non-inclusive start and end times of the gaps in the packets :return: rotation vector sensor data if it exists, None otherwise

  Expand source code

      def load_apim_rotation_vector_from_list(
              packets: List[api_m.RedvoxPacketM], gaps: List[Tuple[float, float]]
      ) -> Optional[SensorData]:
          """
          load rotation vector data from a list of redvox packets

          :param packets: packets with data to load
          :param gaps: the list of non-inclusive start and end times of the gaps in the packets
          :return: rotation vector sensor data if it exists, None otherwise
          """
          return load_xyz_from_list(
              packets,
              gaps,
              SensorType.ROTATION_VECTOR,
          )

`def load_apim_single_sensor_from_list(sensor_type: SensorType, timestamps: List[float], data: List[float], gaps: List[Tuple[float, float]], column_name: str, description: str, sample_interval_micros: float) ‑> Optional[SensorData]`

  Create a single channel sensor of sensor_type with the column name, timestamps, data, and description

  :param sensor_type: the SensorType of sensor to create :param timestamps: the timestamps of the data :param data: the list of data to be added :param gaps: the list of non-inclusive start and end times of the gaps in the packets :param column_name: the name of the columns that contain the data :param description: the description of the sensor :param sample_interval_micros: known sample interval in microseconds :return:

  Expand source code

      def load_apim_single_sensor_from_list(
              sensor_type: SensorType,
              timestamps: List[float],
              data: List[float],
              gaps: List[Tuple[float, float]],
              column_name: str,
              description: str,
              sample_interval_micros: float,
      ) -> Optional[SensorData]:
          """
          Create a single channel sensor of sensor_type with the column name, timestamps, data, and description

          :param sensor_type: the SensorType of sensor to create
          :param timestamps: the timestamps of the data
          :param data: the list of data to be added
          :param gaps: the list of non-inclusive start and end times of the gaps in the packets
          :param column_name: the name of the columns that contain the data
          :param description: the description of the sensor
          :param sample_interval_micros: known sample interval in microseconds
          :return:
          """
          if len(timestamps) > 0:
              data_df: pd.DataFrame = pd.DataFrame(
                  np.transpose([timestamps, timestamps, data]),
                  columns=["timestamps", "unaltered_timestamps", column_name],
              )
              return SensorData(
                  description,
                  gpu.fill_gaps(data_df, gaps, sample_interval_micros, True),
                  sensor_type,
                  calculate_stats=True,
              )
          return None

`def load_apim_xyz_sensor_from_list(sensor_type: SensorType, data: List[List[float]], gaps: List[Tuple[float, float]], column_name: str, description: str, sample_interval_micros: float) ‑> Optional[SensorData]`

  create a three channel sensor of sensor_type with the column name, description, and data given.

  :param sensor_type: the SensorType of sensor to create :param data: the list of data to be added; requires timestamps to be the first list :param gaps: the list of non-inclusive start and end times of the gaps in the packets :param column_name: the name of the columns that contain the data :param description: the description of the sensor :param sample_interval_micros: known sample interval in microseconds :return: SensorData object or None if no data

  Expand source code

      def load_apim_xyz_sensor_from_list(
              sensor_type: SensorType,
              data: List[List[float]],
              gaps: List[Tuple[float, float]],
              column_name: str,
              description: str,
              sample_interval_micros: float,
      ) -> Optional[SensorData]:
          """
          create a three channel sensor of sensor_type with the column name, description, and data given.

          :param sensor_type: the SensorType of sensor to create
          :param data: the list of data to be added; requires timestamps to be the first list
          :param gaps: the list of non-inclusive start and end times of the gaps in the packets
          :param column_name: the name of the columns that contain the data
          :param description: the description of the sensor
          :param sample_interval_micros: known sample interval in microseconds
          :return: SensorData object or None if no data
          """
          if len(data[0]) > 0:
              data_df: pd.DataFrame = pd.DataFrame(
                  np.transpose([data[0], data[0], data[1], data[2], data[3]]),
                  columns=[
                      "timestamps",
                      "unaltered_timestamps",
                      f"{column_name}_x",
                      f"{column_name}_y",
                      f"{column_name}_z",
                  ],
              )
              return SensorData(
                  description,
                  gpu.fill_gaps(data_df, gaps, sample_interval_micros, True),
                  sensor_type,
                  calculate_stats=True,
              )
          return None

`def load_single(packet: src.redvox_api_m.redvox_api_m_pb2.RedvoxPacketM, sensor_type: SensorType) ‑> Optional[SensorData]`
  Expand source code

      def load_single(
              packet: api_m.RedvoxPacketM,
              sensor_type: SensorType,
      ) -> Optional[SensorData]:
          field_name: str = __SENSOR_TYPE_TO_FIELD_NAME[sensor_type]
          sensor_fn: Optional[
              Callable[[api_m.RedvoxPacketM], Sensor]
          ] = __SENSOR_TYPE_TO_SENSOR_FN[sensor_type]
          if __has_sensor(packet, field_name) and sensor_fn is not None:
              sensor = sensor_fn(packet)
              data_df = read_apim_single_sensor(sensor, field_name)
              sample_rate, sample_interval, sample_interval_std = get_sample_statistics(
                  data_df
              )
              return SensorData(
                  sensor.sensor_description,
                  data_df,
                  sensor_type,
                  sample_rate,
                  sample_interval,
                  sample_interval_std,
                  False,
              )

`def load_single_from_list(packets: List[src.redvox_api_m.redvox_api_m_pb2.RedvoxPacketM], gaps: List[Tuple[float, float]], sensor_type: SensorType) ‑> Optional[SensorData]`
  Expand source code

      def load_single_from_list(
              packets: List[api_m.RedvoxPacketM],
              gaps: List[Tuple[float, float]],
              sensor_type: SensorType,
      ) -> Optional[SensorData]:
          field_name: str = __SENSOR_TYPE_TO_FIELD_NAME[sensor_type]
          data_list: List[float] = []
          timestamps: List[float] = []
          sensor_stats: StatsContainer = StatsContainer(f"{field_name}_sensor")
          sensor_fn: Optional[
              Callable[[api_m.RedvoxPacketM], Sensor]
          ] = __SENSOR_TYPE_TO_SENSOR_FN[sensor_type]
          for packet in packets:
              if __has_sensor(packet, field_name) and sensor_fn is not None:
                  sensor: api_m.RedvoxPacketM.Sensors.Single = sensor_fn(packet)
                  data_list.extend(sensor.samples.values)
                  ts = sensor.timestamps.timestamps
                  timestamps.extend(ts)
                  if len(sensor.timestamps.timestamps) == 1:
                      sensor_stats.add(__packet_duration_us(packet), 0, 1)
                  else:
                      sensor_stats.add(np.mean(np.diff(ts)), np.std(np.diff(ts)), len(ts) - 1)
          if len(data_list) > 0:
              return load_apim_single_sensor_from_list(
                  sensor_type,
                  timestamps,
                  data_list,
                  gaps,
                  field_name,
                  get_sensor_description_list(packets, sensor_type),
                  sensor_stats.mean_of_means(),
              )
          return None

`def load_xyz(packet: src.redvox_api_m.redvox_api_m_pb2.RedvoxPacketM, sensor_type: SensorType)`
  Expand source code

      def load_xyz(
              packet: api_m.RedvoxPacketM,
              sensor_type: SensorType,
      ):
          field_name: str = __SENSOR_TYPE_TO_FIELD_NAME[sensor_type]
          sensor_fn: Optional[
              Callable[[api_m.RedvoxPacketM], Sensor]
          ] = __SENSOR_TYPE_TO_SENSOR_FN[sensor_type]
          if __has_sensor(packet, field_name) and sensor_fn is not None:
              sensor = sensor_fn(packet)
              data_df = read_apim_xyz_sensor(sensor, field_name)
              sample_rate, sample_interval, sample_interval_std = get_sample_statistics(
                  data_df
              )
              return SensorData(
                  sensor.sensor_description,
                  data_df,
                  sensor_type,
                  sample_rate,
                  sample_interval,
                  sample_interval_std,
                  False,
              )

`def load_xyz_from_list(packets: List[src.redvox_api_m.redvox_api_m_pb2.RedvoxPacketM], gaps: List[Tuple[float, float]], sensor_type: SensorType) ‑> Optional[SensorData]`
  Expand source code

      def load_xyz_from_list(
              packets: List[api_m.RedvoxPacketM],
              gaps: List[Tuple[float, float]],
              sensor_type: SensorType,
      ) -> Optional[SensorData]:
          field_name: str = __SENSOR_TYPE_TO_FIELD_NAME[sensor_type]
          data_list: List[List[float]] = [[], [], [], []]
          sensor_stats: StatsContainer = StatsContainer(f"{field_name}_sensor")
          packet: api_m.RedvoxPacketM
          sensor_fn: Optional[
              Callable[[api_m.RedvoxPacketM], Sensor]
          ] = __SENSOR_TYPE_TO_SENSOR_FN[sensor_type]
          for packet in packets:
              if __has_sensor(packet, field_name) and sensor_fn is not None:
                  sensor: api_m.RedvoxPacketM.Sensors.Xyz = sensor_fn(packet)

                  ts = sensor.timestamps.timestamps
                  data_list[0].extend(ts)
                  data_list[1].extend(sensor.x_samples.values)
                  data_list[2].extend(sensor.y_samples.values)
                  data_list[3].extend(sensor.z_samples.values)
                  if len(sensor.timestamps.timestamps) == 1:
                      sensor_stats.add(__packet_duration_us(packet), 0, 1)
                  else:
                      sensor_stats.add(np.mean(np.diff(ts)), np.std(np.diff(ts)), len(ts) - 1)
          if len(data_list[0]) > 0:
              return load_apim_xyz_sensor_from_list(
                  sensor_type,
                  data_list,
                  gaps,
                  field_name,
                  get_sensor_description_list(packets, sensor_type),
                  sensor_stats.mean_of_means(),
              )
          return None

`def read_apim_single_sensor(sensor: src.redvox_api_m.redvox_api_m_pb2.Single, column_id: str) ‑> pandas.core.frame.DataFrame`

  read a sensor that has a single data channel from an api M data packet raises Attribute Error if sensor does not contain exactly one data channel

  :param sensor: the single channel api M sensor to read :param column_id: string, used to name the columns :return: Dataframe representing the data in the sensor

  Expand source code

      def read_apim_single_sensor(
              sensor: api_m.RedvoxPacketM.Sensors.Single, column_id: str
      ) -> pd.DataFrame:
          """
          read a sensor that has a single data channel from an api M data packet
          raises Attribute Error if sensor does not contain exactly one data channel

          :param sensor: the single channel api M sensor to read
          :param column_id: string, used to name the columns
          :return: Dataframe representing the data in the sensor
          """
          timestamps: np.ndarray = np.array(sensor.timestamps.timestamps)
          try:
              columns: List[str] = ["timestamps", "unaltered_timestamps", column_id]
              return pd.DataFrame(
                  np.transpose([timestamps, timestamps, np.array(sensor.samples.values)]),
                  columns=columns,
              )
          except AttributeError:
              raise

`def read_apim_xyz_sensor(sensor: src.redvox_api_m.redvox_api_m_pb2.Xyz, column_id: str) ‑> pandas.core.frame.DataFrame`

  read a sensor that has xyz data channels from an api M data packet raises Attribute Error if sensor does not contain xyz channels

  :param sensor: the xyz api M sensor to read :param column_id: string, used to name the columns :return: Dataframe representing the data in the sensor

  Expand source code

      def read_apim_xyz_sensor(
              sensor: api_m.RedvoxPacketM.Sensors.Xyz, column_id: str
      ) -> pd.DataFrame:
          """
          read a sensor that has xyz data channels from an api M data packet
          raises Attribute Error if sensor does not contain xyz channels

          :param sensor: the xyz api M sensor to read
          :param column_id: string, used to name the columns
          :return: Dataframe representing the data in the sensor
          """
          timestamps: np.ndarray = np.array(sensor.timestamps.timestamps)
          try:
              columns: List[str] = [
                  "timestamps",
                  "unaltered_timestamps",
                  f"{column_id}_x",
                  f"{column_id}_y",
                  f"{column_id}_z",
              ]
              return pd.DataFrame(
                  np.transpose(
                      [
                          timestamps,
                          timestamps,
                          np.array(sensor.x_samples.values),
                          np.array(sensor.y_samples.values),
                          np.array(sensor.z_samples.values),
                      ]
                  ),
                  columns=columns,
              )
          except AttributeError:
              raise