Instructions:
The project focuses on the use of FMCW radar.
It is used for data collection, point detection, tracking and other functions.
The project is broken down into several modules.
The aim of this division is to decouple as much as possible the functionalities present in each module, and make the development of new functionalities more flexible and rapid.

Some information will be presented to you in json format:
- module: the name of the module
- module_structure: the structure of the module (the files making up the module and their hierarchy)
- module_files_already_generated_doc: the documentation of the module's files if already generated
- other_modules_doc: the documentation of the other modules on which the module depends (possibly none at all)
- gen_doc_of_file: the file you will be given to generate the documentation of. If no file is given, the documentation of the entire module must be generated.
Your goal is to create a markdown documentation of the file you will be given, or the entire module if no file is given.
This documentation is intended to guide a developer who is new to the project.
You can therefore add whatever you feel is relevant (data flow, functionalities, etc.).

Informations:
{"module": "radar_toolbox", "module_structure": "radar_toolbox/fmcw_utils.py;radar_toolbox/utils.py;radar_toolbox\\capture_session/main.py;radar_toolbox\\conf/capture_conf.json;radar_toolbox\\conf/profile_vs1642_exp.cfg;radar_toolbox\\conf/raw_bin_to_numpy.json;radar_toolbox\\raw_data/adc.py;radar_toolbox\\raw_data/raw_bin_to_numpy.py;radar_toolbox\\raw_data/record_raw_data.py;radar_toolbox\\TI_data/exceptions.py;radar_toolbox\\TI_data/record_ti_data.py;radar_toolbox\\TI_data/sensor_com.py;", "module_files_already_generated_doc": [], "other_modules_doc": null, "gen_doc_of_file": {"path": "radar_toolbox\\fmcw_utils.py", "content": "import math\nfrom enum import Enum\n\nimport numpy as np\n\nC0 = 299792458  # Speed of light\n\n\nclass RadarModel(Enum):\n    IWR1642 = 0\n    IWR6843AOP = 1\n    IWRL6432 = 2\n\n    @staticmethod\n    def get_model_from_str(string):\n        if string == \"6843\":\n            return RadarModel.IWR6843AOP\n        elif string == \"1642\":\n            return RadarModel.IWR1642\n        elif string == \"6432\":\n            return RadarModel.IWRL6432\n        else:\n            raise NotImplementedError(f\"Radar model {string} not implemented.\")\n\n\ndef get_phase_compensation_for_radar_model(model: RadarModel):\n    if model == RadarModel.IWR1642:\n        return None\n    elif model == RadarModel.IWR6843AOP:\n        return np.array([[0, 0, 0, 0],\n                         [np.pi, np.pi, np.pi, np.pi],\n                         [0, 0, 0, 0],\n                         [np.pi, np.pi, np.pi, np.pi]])\n    else:\n        raise NotImplementedError(f\"Model {model} not implemented.\")\n    # TODO: voir param compRangeBiasAndRxChanPhase dans les config g\u00c3\u00a9n\u00c3\u00a9r\u00c3\u00a9es pour le IWRL6432\n\n\ndef load_IQ_file(path, is_complex: bool, nb_samples: int = 256, nb_chirps: int = 64):\n    if is_complex:\n        data = np.fromfile(path + \"i.bin\", dtype=np.int16) + complex(0, 1) * np.fromfile(path + \"q.bin\", dtype=np.int16)\n    else:\n        data = np.fromfile(path + \"i.bin\", dtype=np.int16)\n\n    # Drop potential incomplete frames\n    frames_to_remove = len(data) % (nb_samples * nb_chirps)\n    if frames_to_remove > 0:\n        data = data[:-frames_to_remove]\n\n    return data.reshape(-1, nb_chirps, nb_samples)\n\n\ndef load_IQ_data(path_prefix, is_complex: bool, nb_antennas: int, nb_samples: int = 256,\n                 nb_chirps: int = 64) -> np.ndarray:\n    \"\"\"\n    :param path_prefix: Path's prefix of the IQ files. Will try to read the files \"[path]ki.bin\" and \"[path]kq.bin\"\n        where k is the antenna number.\n    :param is_complex: Is data complex ? If True, we load I and Q files, eles only I.\n    :param nb_antennas: How many antennas data to load ?\n    :param nb_samples: How many samples per chirp.\n    :param nb_chirps: How many chirps per frame.\n    :return: an array of shape (nb_antennas, frames, chirps, samples).\n    \"\"\"\n    data = None\n\n    for antenna_idx in range(nb_antennas):\n        antenna_data = load_IQ_file(path_prefix + str(antenna_idx), is_complex=is_complex, nb_samples=nb_samples,\n                                    nb_chirps=nb_chirps)\n        if data is None:\n            if is_complex:\n                data = np.empty((nb_antennas,) + antenna_data.shape, dtype=np.complex64)\n            else:\n                data = np.empty((nb_antennas,) + antenna_data.shape)\n        data[antenna_idx] = antenna_data\n\n    return data\n\n\nPACKET_DATA_FULL_SIZE = 1456  # Size in bytes of a (full) packet's data\nPACKETS_NUMBER_BYTE_SIZE = 4  # Total number of packets (without duplicates and with missing packets) will be stored into the generated bin file. This variable is its size in bytes.\n\n\ndef raw_capture_bin_preparation(capture_bin_file_path: str,\n                                cfg_file_path: str,\n                                radar: RadarModel,\n                                output_bin_file_path: str = None,\n                                verbose=True):\n    \"\"\"\n    Creates a new binary file from a given binary capture file.\n    This new file will contain the number of packets, the reordered packets data (with duplicates removal) and an array\n    of corrupted chirps (data loss).\n\n    :param capture_bin_file_path: The binary file full of packets acquired during the capture session.\n    :param cfg_file_path: path of the config file used for the capture session.\n    :param radar: Radar model used for the capture file.\n    :param output_bin_file_path: (Optional) Path of the generated binary file. If \"None\", the path used is the same as\n        capture_bin_file_path with a \"_prepared\"\n    \"\"\"\n    config = read_cfg_file(cfg_file_path, radar)\n    nb_samples = config[\"nb_samples\"]\n    nb_loops = config[\"nb_loops\"]\n    nb_channels = config[\"nb_channels\"]\n    nb_chirps_per_frame = nb_channels * nb_loops\n    chirp_byte_size = 2 * 2 * nb_samples  # 2 * 2 is because we have I and Q values of 2 bytes each\n    frame_byte_size = nb_loops * chirp_byte_size * nb_channels  # Size of an entire frame in bytes\n    received_seq_number = set()\n    total_missing_packets = 0  # Number of lost packet\n    nb_losses = 0  # Number of times packets were lost (multiple packets can be lost in a single time)\n    last_packet_size = PACKET_DATA_FULL_SIZE\n\n    if output_bin_file_path == None:\n        output_bin_file_path = capture_bin_file_path[:-4] + \"_prepared.bin\"\n\n    with open(capture_bin_file_path, 'rb') as input_file:\n        with open(output_bin_file_path, 'wb') as output_file:\n            \"\"\"\n            We read all the packets inside the capture_bin_file_path binary file.\n            We create a binary file at output_bin_file_path that will get (in this order): the number of packets,\n            every packet's data and an array of corrupted chirps (chirps where data hasn't been received due to missing\n            packets).\n            These packets' data are written at a specific position in the file: every data is ordered according to its\n            packet sequence number.\n            Duplicates aren't written.\n            Missing packets are just places in the output file where we won't write, so the data will be zeros there.\n            \"\"\"\n            while True:\n                # Header is made of 3 values, encoded in little endian\n                # A sequence number (4 bytes) (starts at 1, not 0)\n                seq = int.from_bytes(input_file.read(4), 'little')\n                # A data length field (4 bytes)\n                data_len = int.from_bytes(input_file.read(4), 'little')\n                # A byte count (6 bytes), not used here\n                input_file.read(6)\n\n                if data_len == 0:  # End of file, no more data to read\n                    break\n                if data_len > PACKET_DATA_FULL_SIZE:\n                    raise ValueError(\n                        f\"data length field greater than max data length ({PACKET_DATA_FULL_SIZE}): {data_len}\")\n\n                # Read the data and keep it as bytes\n                data = input_file.read(data_len)\n\n                # If packet sequence has already been read, we don't want to store this packet again so we skip this one.\n                if seq in received_seq_number:\n                    continue\n                received_seq_number.add(seq)\n\n                # We write at a position that directly depends on the sequence number + packets_number_byte_size offset\n                sequence_position = (seq - 1) * PACKET_DATA_FULL_SIZE\n                output_file.seek(sequence_position + PACKETS_NUMBER_BYTE_SIZE)\n                output_file.write(data)\n\n                if data_len < PACKET_DATA_FULL_SIZE:\n                    \"\"\"\n                    The only packet that could contain less data than its maximum possible size is the last packet.\n                    \"\"\"\n                    last_packet_size = data_len\n                    nb_missing_bytes = PACKET_DATA_FULL_SIZE - data_len\n                    output_file.write((0).to_bytes(nb_missing_bytes, 'little'))\n\n    received_seq_number = np.array(list(received_seq_number))\n    prev_seq = 0\n    byte_count = 0\n    nb_packets = received_seq_number.max()\n    total_data_captured = (nb_packets - 1) * PACKET_DATA_FULL_SIZE + last_packet_size\n    nb_frames = math.ceil(total_data_captured / frame_byte_size)\n    corrupted_chirps = np.zeros((nb_channels, nb_frames, nb_loops), dtype=bool)\n    # We loop over each received sequence number in ascending order to detect and handle missing packets\n    for idx in received_seq_number.argsort():\n        seq = received_seq_number[idx]\n        nb_missing_packets = seq - prev_seq - 1\n        if nb_missing_packets != 0:  # If packets have been lost\n            nb_losses += 1\n            total_missing_packets += nb_missing_packets\n            missing_bytes = PACKET_DATA_FULL_SIZE * nb_missing_packets\n            # Index of the chirp where the loss starts\n            loss_chirp_idx_start = byte_count // chirp_byte_size\n            # Index of the chirp where the loss ends (floor int value)\n            loss_chirp_idx_end = (byte_count + missing_bytes) // chirp_byte_size\n            if (byte_count + missing_bytes) % chirp_byte_size != 0:\n                loss_chirp_idx_end += 1\n\n            for global_chirp_idx in range(loss_chirp_idx_start, loss_chirp_idx_end):\n                frame_idx = global_chirp_idx // nb_chirps_per_frame\n                chirp_index_of_channel = (global_chirp_idx % nb_chirps_per_frame) // nb_channels\n                channel_idx = (global_chirp_idx % nb_chirps_per_frame) % nb_channels\n                # Keeping track of chirps impacted by the loss\n                corrupted_chirps[channel_idx, frame_idx, chirp_index_of_channel] = True\n            byte_count += PACKET_DATA_FULL_SIZE * nb_missing_packets\n        prev_seq = seq\n        byte_count += PACKET_DATA_FULL_SIZE\n\n    corrupted_channels_frames = np.any(corrupted_chirps, axis=-1)\n    corrupted_frames = np.any(corrupted_channels_frames, axis=0)\n    nb_corrupted_frames = np.sum(corrupted_frames)\n    if verbose:\n        print(f\"Reading capture file {capture_bin_file_path}\")\n        print(f\"Writing output file {output_bin_file_path}\")\n        print(f\"Number of frames: {nb_frames}\")\n        print(\"Losses:\")\n        print(\n            f\"\\tNumber of packets lost: {total_missing_packets} = {round(100 * total_missing_packets / nb_packets, 2)}%\")\n        print(f\"\\tNumber of losses: {nb_losses}\")\n        print(\n            f\"\\tNumber of corrupted frames: {nb_corrupted_frames} = {round(100 * nb_corrupted_frames / nb_frames, 2)}%\")\n\n    # We re-open the output bin file to store the number of packets and the corrupted chirps array\n    with open(output_bin_file_path, 'r+b') as output_file:\n        # number of packets stored at the beginning of the file\n        output_file.seek(0)\n        output_file.write(int(nb_packets).to_bytes(PACKETS_NUMBER_BYTE_SIZE, \"little\"))\n        # corrupted chirps array stored at the end of the file, after the packets' data\n        end_of_file_position = nb_packets * PACKET_DATA_FULL_SIZE + PACKETS_NUMBER_BYTE_SIZE\n        output_file.seek(end_of_file_position)\n        output_file.write(corrupted_chirps.tobytes())\n\n\ndef prepared_bin_to_numpy(bin_file_path: str, cfg_file_path: str, radar: RadarModel, nb_lanes: int = 2):\n    \"\"\"\n    Returns the radar data contained in a raw .bin file.\n    Packets can be missing due to network issues. Frames concerned by these losses are dropped.\n\n    :param bin_file_path: the path of the raw binary data of the recorded radar session.\n    :param cfg_file_path: path of the config file\n    :param nb_lanes: number of lanes (check your radar config).\n    :return: extracted data in a complex array of shape (channels, frames, chirps, samples)\n        and a bool array of shape (channels, frames, chirps) with True values for corrupted chirps.\n    \"\"\"\n    config = read_cfg_file(cfg_file_path, radar)\n    nb_samples = config[\"nb_samples\"]\n    nb_loops = config[\"nb_loops\"]\n    nb_channels = config[\"nb_channels\"]\n    value_byte_size = 2\n    nb_values_per_frame = nb_samples * 2 * nb_channels * nb_loops  # \"2\" is because we have I and Q values\n    frame_byte_size = nb_values_per_frame * value_byte_size  # Size of an entire frame in bytes\n\n    with open(bin_file_path, 'rb') as f:\n        # Reading number of packets\n        nb_packets = int.from_bytes(f.read(PACKETS_NUMBER_BYTE_SIZE), \"little\")\n        total_data_captured = nb_packets * PACKET_DATA_FULL_SIZE\n        nb_frames = math.ceil(total_data_captured / frame_byte_size)\n        # Reading packets' data\n        packets_data = np.zeros((nb_packets, PACKET_DATA_FULL_SIZE // value_byte_size), dtype=\"i2\")\n        for packet_idx in range(nb_packets):\n            # Read and convert the data to a numpy array\n            data = np.frombuffer(f.read(PACKET_DATA_FULL_SIZE), dtype=\"<i2\")  # 2 bytes little endian\n            packets_data[packet_idx] = data\n        # Reading corrupted chirps array\n        corrupted_chirps = get_corrupted_chirps(f, nb_channels, nb_frames, nb_loops)\n\n    flatten_data = packets_data.flatten()\n\n    # We check if last frame is complete\n    frames_rest = len(flatten_data) % nb_values_per_frame\n    if frames_rest > 0:  # If not complete\n        flatten_data = flatten_data[:-frames_rest]  # We don't want the last frame if it is not complete\n        corrupted_chirps = corrupted_chirps[:, :-1]\n        nb_frames -= 1\n\n    raw_data = flatten_data.reshape((nb_frames, nb_loops * nb_channels, nb_samples * 2))\n    print(\"data shape : \", (nb_channels, nb_frames, nb_loops, nb_samples))\n    data_complex = np.empty((nb_channels, nb_frames, nb_loops, nb_samples), dtype=np.complex64)\n\n    # See mmWave Studio doc, section \"Case of IWR1642 with DCA1000 \u00e2\u20ac\u201c complex I/Q, 4 channels, 2 LVDS lanes\" to\n    # understand how IQ values are ordered.\n    # Be careful, this documentation doesn't describe well how channels are ordered. Check instead the new capture\n    # format array in the mmWave Radar Demo doc.\n    # Data received by xWR16xx and IWR6843AOP is necessarily non-interleaved.\n\n    indices_imag, indices_real = get_DCA1000_indices_imag_real(nb_lanes, nb_samples)\n\n    if radar == RadarModel.IWR6843AOP or radar == RadarModel.IWR1642:\n        for frame_idx, frame in enumerate(raw_data):\n            for i, chirp in enumerate(frame):\n                chirp_index = i // nb_channels\n                channel_idx = i % nb_channels\n                data_complex[channel_idx, frame_idx, chirp_index] = chirp[indices_real] + 1j * chirp[indices_imag]\n    else:\n        raise NotImplementedError(f\"Radar model {radar} not implemented\")\n\n    # # Chirps visualization\n    # columns = 6\n    # fig, ax = plt.subplots(1, columns)\n    # for chirp in range(columns):\n    #     test = data_complex[:, 4, chirp, -10:]\n    #     for c in test:\n    #         ax[chirp].plot(c.imag)\n    #     ax[chirp].title.set_text(f\"Chirp {chirp}\")\n    # plt.show()\n\n    return data_complex, corrupted_chirps\n\n\ndef get_corrupted_chirps(f, nb_channels, nb_frames, nb_loops):\n    size_of_numpy_bool = np.array([True], dtype=bool).nbytes\n    corrupted_chirps_bytes = size_of_numpy_bool * nb_channels * nb_frames * nb_loops\n    corrupted_chirps = np.frombuffer(f.read(corrupted_chirps_bytes), dtype=bool)\n    corrupted_chirps = np.reshape(corrupted_chirps, (nb_channels, nb_frames, nb_loops))\n    return corrupted_chirps\n\n\ndef get_DCA1000_indices_imag_real(nb_lanes, nb_samples):\n    indices_real = np.empty(nb_samples, dtype=int)\n    i = 0\n    while i < nb_samples:\n        for j in range(nb_lanes):\n            indices_real[i + j] = i * 2 + j\n        i += nb_lanes\n    indices_imag = indices_real + nb_lanes\n    return indices_imag, indices_real\n\n\ndef coordinates_from_range_and_angles(azimuths: np.ndarray, elevations: np.ndarray, ranges: np.ndarray):\n    x = ranges * np.sin(azimuths)\n    y = ranges * np.sin(elevations)\n    tmp = ranges * ranges - x * x - y * y\n    tmp[np.abs(tmp) < 1e-4] = 0  # To avoid \"nan\" value with sqrt\n    z = np.sqrt(tmp)\n    return np.array([x, y, z]).swapaxes(0, 1)\n\n\nclass PointsAttributes(Enum):\n    RANGE = 0\n    AZIMUTH = 1\n    ELEVATION = 2\n    VELOCITY = 3\n\n    @staticmethod\n    def points_attributes_abs(points: np.ndarray, attribute_idx: int):\n        points[:, attribute_idx] = np.abs(points[:, attribute_idx])\n\n\nclass AntennaDataSaver:\n    def __init__(self,\n                 save_abs_1D_FFTs: bool = False,\n                 save_abs_2D_FFTs: bool = False):\n        self.save_abs_1D_FFTs = save_abs_1D_FFTs\n        self.abs_1D_FFTs = []\n        self.save_abs_2D_FFTs = save_abs_2D_FFTs\n        self.abs_2D_FFTs = []\n\n    def add_abs_1D_FFTs(self, FFTs_1D):\n        self.abs_1D_FFTs.append(FFTs_1D)\n\n    def add_abs_2D_FFTs(self, FFTs_2D):\n        self.abs_2D_FFTs.append(FFTs_2D)\n\n\nclass RadarDataSaver:\n    def __init__(self, save_common_detection_matrices: bool = False):\n        self.save_common_detection_matrices = save_common_detection_matrices\n        self.common_detection_matrices = []\n\n    def add_common_detection_matrices(self, common_detection_matrices):\n        self.common_detection_matrices.append(common_detection_matrices)\n\n\ndef describe_config(config):\n    print(\"====== Config File ======\")\n    print()\n    print(\"--- Profile ---\")\n    print()\n    print(f\"\\tStart frequency: {config['start_freq']} Hz\")\n    print(f\"\\tIdle time: {config['idle_time']} s\")\n    print(f\"\\tADC start time: {config['adc_start_time']} s\")\n    print(f\"\\tRamp end time: {config['ramp_end_time']} s\")\n    print(f\"\\tSlope: {config['slope']} Hz/s\")\n    print(f\"\\tNumber of samples: {config['nb_samples']}\")\n    print(f\"\\tSampling frequency: {config['sampling_freq']} samples/s\")\n    print(f\"\\tChirp duration: {config['chirp_duration']} s\")\n    print()\n    print(\"--- Frame ---\")\n    print()\n    print(f'\\tLoop start index: {config[\"loop_start_index\"]}')\n    print(f'\\tLoop end index: {config[\"loop_end_index\"]}')\n    print(f'\\tNumber of chirps: {config[\"nb_chirps\"]}')\n    print(f'\\tNumber of loops: {config[\"nb_loops\"]}')\n    print(f'\\tFrame periodicity: {config[\"frame_periodicity\"]} s')\n    print()\n    print(\"--- Antennas ---\")\n    print()\n    print(f\"\\tTX mask: {config['antennas']['TX_mask']}\")\n    print(f\"\\tRX mask: {config['antennas']['RX_mask']}\")\n    print()\n    print(\"\\tTX pattern:\")\n    print(config[\"TX_pattern\"])\n    print()\n    print(\"\\tRX pattern:\")\n    print(config[\"RX_pattern\"])\n    print()\n    print(\"--- Loop ---\")\n    print()\n    print(\"\\tchirps:\")\n    for chirp_idx in config[\"loop_chirps\"]:\n        chirp = config[\"loop_chirps\"][chirp_idx]\n        print(f\"\\tStart index: {chirp['start_index']} | Tx mask: {chirp['tx_mask']}\")\n    print()\n    print(f'\\tVirtual antennas order in loop:')\n    print(config[\"virtual_antennas_order_in_loop\"])\n    print()\n    print(\"\\tVirtual antennas pattern\")\n    print(config[\"virtual_antennas_pattern\"])\n    print()\n    print(\"====== End Config File ======\\n\\n\")\n\n\ndef read_cfg_file(path: str, radar_model: RadarModel, verbose: int = 0):\n    \"\"\"\n    Load a .cfg file.\n    :param path: Configuration file path.\n    :param radar_model: Radar's model.\n    :param verbose: 1 or 0, ether or not you want a description of the loaded config file.\n    :return: a dictionary containing config parameters.\n    \"\"\"\n    if radar_model not in RadarModel:\n        raise Exception(f\"{radar_model} model not implemented.\")\n    config = {}\n    with open(path, 'r') as f:\n        loop_chirps = {}  # Will store chirps making the emission loop\n        for line in f:\n            params = line.split()\n            command = params[0]\n            if command == \"channelCfg\":\n                if radar_model == RadarModel.IWR6843AOP:\n                    config[\"antennas\"] = {\"TX_mask\": format(int(params[2]), '03b'),  # 3 bits mask\n                                          \"RX_mask\": format(int(params[1]), '04b')}  # 4 bits mask\n                elif radar_model == RadarModel.IWR1642:\n                    config[\"antennas\"] = {\"TX_mask\": format(int(params[2]), '02b'),  # 2 bits mask\n                                          \"RX_mask\": format(int(params[1]), '04b')}  # 4 bits mask\n                else:\n                    raise Exception(f\"{radar_model} model not implemented.\")\n\n                config[\"nb_TX\"] = format(int(params[2]), 'b').count(\"1\")\n                config[\"nb_RX\"] = format(int(params[1]), 'b').count(\"1\")\n            elif command == \"profileCfg\":\n                config[\"start_freq\"] = float(params[2]) * 1e9  # GHz\n                config[\"idle_time\"] = float(params[3]) * 1e-6  # us\n                config[\"adc_start_time\"] = float(params[4]) * 1e-6  # us\n                config[\"ramp_end_time\"] = float(params[5]) * 1e-6  # us\n                config[\"slope\"] = float(params[8]) * 1e12  # MHz/us ( 1e6 / 1e-6 = 1e12 )\n                config[\"nb_samples\"] = int(params[10])\n                config[\"sampling_freq\"] = int(params[11]) * 1e3  # k-samples/s\n                config[\"chirp_duration\"] = config[\"idle_time\"] + config[\"ramp_end_time\"]\n\n                config[\"adc_sampling_duration\"] = config[\"nb_samples\"] / config[\"sampling_freq\"]\n                # TODO Checking if timing are not to small\n\n            elif command == \"frameCfg\":\n                config[\"loop_start_index\"] = int(params[1])\n                config[\"loop_end_index\"] = int(params[2])\n                config[\"nb_chirps\"] = config[\"loop_end_index\"] - config[\"loop_start_index\"] + 1\n                config[\"nb_loops\"] = int(params[3])\n                config[\"frame_periodicity\"] = float(params[5]) * 1e-3  # ms\n            elif command == \"adcCfg\":\n                config[\"num_adc_bits\"] = int(params[1])\n                assert config[\"num_adc_bits\"] == 2, \"numADCBits other than 2 (16 bits) not supported.\"\n                config[\"adc_format\"] = int(params[2])\n            elif command == \"chirpCfg\":\n                chirp_start_index = int(params[1])\n                tx_mask = int(params[8])\n                binary_tx_mask = format(tx_mask, 'b')\n                # log10_mask = math.log10(int(binary_tx_mask))\n                # tx_idx = int(log10_mask)\n                loop_chirps[chirp_start_index] = {\"tx_mask\": binary_tx_mask}\n                loop_chirps[chirp_start_index][\"start_index\"] = chirp_start_index\n\n        nb_channels_loop = 0  # number of channels in one frame\n        for chirp_idx in range(config[\"loop_start_index\"],\n                               config[\"loop_end_index\"] + 1):  # For every chirp that will be emitted during a loop\n            if chirp_idx not in loop_chirps:\n                raise Exception(\n                    f\"Chirp index {chirp_idx} not declared in any chirpCfg command but used in frameCfg command.\")\n            chirp = loop_chirps[chirp_idx]\n            nb_tx_enabled = chirp[\"tx_mask\"].count(\"1\")  # Number of TX enabled for this chirp\n            assert nb_tx_enabled == 1, \"More than one TX antenna in a single chirp is not yet handled\"\n            nb_channels_chirp = config[\n                                    \"nb_RX\"] * nb_tx_enabled  # This chirp consists of [nb RX antennas multiplied by nb of enabled TX antennas] channels\n            nb_channels_loop += nb_channels_chirp  # Increment the number of channels by this value\n        config[\"nb_channels\"] = nb_channels_loop\n\n        # --- Determining Virtual Antenna Pattern from config ---\n        if radar_model == RadarModel.IWR6843AOP:\n            config[\"h_spacing\"] = 5e-3 / 2\n            config[\"v_spacing\"] = 5e-3 / 2\n            RX_pattern = np.array([[3, 1],\n                                   [2, 0]], dtype=object)\n            TX_pattern = np.array([[0, None, None],\n                                   [None, None, None],\n                                   [2, None, 1]])\n        elif radar_model == RadarModel.IWR1642:\n            config[\"h_spacing\"] = (C0 / 77e9) / 2\n            config[\"v_spacing\"] = (C0 / 77e9) / 2\n            RX_pattern = np.array([[0, 1, 2, 3]], dtype=object)\n            TX_pattern = np.array([[0, None, None, None, 1]])\n        else:\n            raise NotImplemented(f\"{radar_model} not implemented.\")\n\n        nb_removed_RX = 0\n        for idx, bit in enumerate(reversed(config[\"antennas\"][\"RX_mask\"])):\n            if bit == \"0\":\n                RX_pattern[RX_pattern == idx] = None\n                nb_removed_RX += 1\n            else:\n                RX_pattern[RX_pattern == idx] -= nb_removed_RX\n        for idx, bit in enumerate(reversed(config[\"antennas\"][\"TX_mask\"])):\n            if bit == \"0\":\n                TX_pattern[TX_pattern == idx] = None\n        virtual_antenna_shape = (\n            TX_pattern.shape[0] + RX_pattern.shape[0] - 1, TX_pattern.shape[1] + RX_pattern.shape[1] - 1)\n        virtual_antennas_pattern = np.full(virtual_antenna_shape, None)\n        virtual_antennas_order_in_loop = np.full(virtual_antenna_shape, None)\n        for loop_idx, chirp_idx in enumerate(range(config[\"loop_start_index\"], config[\n                                                                                   \"loop_end_index\"] + 1)):  # For every chirp that will be emitted during a loop\n            chirp = loop_chirps[chirp_idx]\n            nb_tx_enabled = chirp[\"tx_mask\"].count(\"1\")  # Number of TX enabled for this chirp\n            assert nb_tx_enabled == 1, \"More than one TX antenna in a single chirp is not yet handled\"\n            log10_mask = math.log10(int(chirp[\"tx_mask\"]))\n            tx_idx = int(log10_mask)\n            if tx_idx not in TX_pattern:\n                raise Exception(\n                    f\"TX index {tx_idx} used in a chirpCfg Command, but this antenna is not enabled or doesn't exist.\")\n            tx_h = np.where(TX_pattern == tx_idx)[1][0]\n            tx_v = np.where(TX_pattern == tx_idx)[0][0]\n            for y, row in enumerate(RX_pattern):\n                for x, rx_idx in enumerate(row):\n                    if rx_idx == None:\n                        continue\n                    virtual_antennas_pattern[tx_v + y, tx_h + x] = rx_idx + config[\"nb_RX\"] * chirp_idx\n                    virtual_antennas_order_in_loop[tx_v + y, tx_h + x] = loop_idx\n\n        config[\"TX_pattern\"] = TX_pattern\n        config[\"RX_pattern\"] = RX_pattern\n        config[\"virtual_antennas_pattern\"] = virtual_antennas_pattern\n        config[\"loop_chirps\"] = loop_chirps\n        config[\"virtual_antennas_order_in_loop\"] = virtual_antennas_order_in_loop\n\n    if verbose: describe_config(config)\n    return config\n\n\ndef safe_memmap(filename, mode, dtype, shape):\n    \"\"\"\n    check if the file is saved using npy format, with json string at the beginning\n    If so, memmap needs an offset\n    \"\"\"\n    offset = 0\n    with open(filename, 'rb') as f:\n        if f.read(6) == b'\\x93NUMPY':\n            offset += 7\n            while f.read(1) != b'\\x0A':\n                offset += 1\n    return np.memmap(filename, dtype, mode, offset, shape)\n\n\nclass Frame:\n    \"\"\"\n    Handles building of a frame from raw data.\n    Because raw data packets may be cross-frame, we fill an array up to the frame size and then start a new one\n    \"\"\"\n\n    def __init__(self,\n                 chirps_per_frame: int,\n                 points_per_chirp: int,\n                 nb_channels: int,\n                 data: list = None,\n                 frame_index: int = 0):\n        \"\"\"\n        Builds a new Frame data handler, either empty or with some data (i.e. extra data from a previous frame)\n        :param chirps_per_frame: number of chirps in a frame.\n        :param points_per_chirp: number of points in a chirp.\n        :param nb_channels: number of channels in a frame.\n        :param data: remaining data from a previous packet (in the case of cross-frame packet).\n        :param frame_index: index of the frame.\n        \"\"\"\n        self.frame_data = data\n        if data is None:\n            self.frame_data = []\n\n        self.chirps_per_frame = chirps_per_frame\n        self.points_per_chirp = points_per_chirp\n        self.nb_channels = nb_channels\n        self.nb_values_in_frame = self.chirps_per_frame * self.points_per_chirp * self.nb_channels * 2\n        self.frame_index = frame_index\n        self.indices_imag, self.indices_real = get_DCA1000_indices_imag_real(nb_lanes=2,\n                                                                             nb_samples=self.points_per_chirp)\n\n    def is_complete(self) -> bool:\n        return len(self.frame_data) > self.get_nb_values_in_frame()\n\n    def add_values(self, data_to_append):\n        \"\"\"\n        Adds some new data to existing frame\n        :param data_to_append: data to add\n        \"\"\"\n        self.frame_data.extend(data_to_append)\n\n    def get_frame_values(self) -> np.ndarray:\n        \"\"\"\n        return frame values, without extra \"cross-frame\" values\n        \"\"\"\n        return np.array(self.frame_data[:self.get_nb_values_in_frame()])\n\n    def get_next_frame(self) -> 'Frame':\n        \"\"\"\n        Builds and returns a new frame with extra data\n        \"\"\"\n        return Frame(chirps_per_frame=self.chirps_per_frame,\n                     points_per_chirp=self.points_per_chirp,\n                     nb_channels=self.nb_channels,\n                     data=self.frame_data[self.get_nb_values_in_frame():],\n                     frame_index=self.frame_index + 1)\n\n    def get_frame_index(self) -> int:\n        return self.frame_index\n\n    def get_nb_values_in_frame(self):\n        \"\"\"\n        Computes the number of values making a frame (chirps_per_frame * points_per_chirp * nb_channels * 2)\n        *2 is because we have to values per point (one for I and one for Q, with value = I + jQ)\n        \"\"\"\n        return self.nb_values_in_frame\n\n    def order_frame(self, flat_unordered_frame: np.ndarray):\n        \"\"\"\n        Converts a flat unordered frame from LVDS output and converts it into a complex order frame of shape\n        (channels, chirps, sampels).\n        \"\"\"\n        flat_unordered_frame = flat_unordered_frame.reshape(self.chirps_per_frame * self.nb_channels,\n                                                            2 * self.points_per_chirp)\n        ordered_frame = np.empty((self.nb_channels, self.chirps_per_frame, self.points_per_chirp), dtype=complex)\n        for i, chirp in enumerate(flat_unordered_frame):\n            chirp_idx = i // self.nb_channels\n            channel_idx = i % self.nb_channels\n            reordered_chirp = chirp[self.indices_real] + 1j * chirp[self.indices_imag]\n            ordered_frame[channel_idx][chirp_idx][:] = reordered_chirp[:]\n        return ordered_frame\n\n\nclass AntennaConfig:\n    def __init__(self, sampling_rate: float, start_frequency: float = 77e9, bandwidth: float = 4e9,\n                 sampling_duration: float = 40e-6, loop_duration: float = 40e-6, nb_loops: int = 64,\n                 nb_samples: int = 256, frame_periodicity: float = 50e-3, is_complex: bool = True):\n        self.fc = start_frequency  # Start frequency\n        self.B = bandwidth  # Bandwidth\n        self.sampling_duration = sampling_duration  # Duration of signal sampling\n        self.loop_duration = loop_duration\n        self.nb_loops = nb_loops  # Number of loops (initially nb_chirps, but the term of chirp is confusing with the \"chirps\" of a loop)\n        self.nb_samples = nb_samples  # Number of samples for each chirp\n        self.Fs = sampling_rate  # Sampling rate\n        self.frame_periodicity = frame_periodicity\n        self.is_complex = is_complex\n\n        self.wavelength = C0 / self.fc\n        self.center_frequency = self.fc + self.B / 2\n        self.center_wavelength = C0 / self.center_frequency\n        self.S = self.B / self.sampling_duration  # Chirp slope\n\n    @staticmethod\n    def get_antenna_parameters_from_config_file(config: dict):\n        \"\"\"\n        Loads antenna config from a .cfg file.\n        \"\"\"\n        bandwidth = config[\"adc_sampling_duration\"] * config[\"slope\"]\n        is_complex = config[\"adc_format\"] != 0\n        loop_duration = config[\"nb_chirps\"] * config[\"chirp_duration\"]\n        start_frequency = config[\"start_freq\"] + config[\"adc_start_time\"] * config[\"slope\"]\n        return AntennaConfig(sampling_rate=config[\"sampling_freq\"],\n                             start_frequency=start_frequency,\n                             bandwidth=bandwidth,\n                             sampling_duration=config[\"adc_sampling_duration\"],\n                             loop_duration=loop_duration,\n                             nb_loops=config[\"nb_loops\"],\n                             nb_samples=config[\"nb_samples\"],\n                             frame_periodicity=config[\"frame_periodicity\"],\n                             is_complex=is_complex)\n"}}