Source code for radarsimpy.transmitter

"""
This script contains classes that define all the parameters for
a radar transmitter

This script requires that 'numpy' be installed within the Python
environment you are running this script in.

---

- Copyright (C) 2018 - PRESENT  radarsimx.com
- E-mail: info@radarsimx.com
- Website: https://radarsimx.com

::

    ██████╗  █████╗ ██████╗  █████╗ ██████╗ ███████╗██╗███╗   ███╗██╗  ██╗
    ██╔══██╗██╔══██╗██╔══██╗██╔══██╗██╔══██╗██╔════╝██║████╗ ████║╚██╗██╔╝
    ██████╔╝███████║██║  ██║███████║██████╔╝███████╗██║██╔████╔██║ ╚███╔╝ 
    ██╔══██╗██╔══██║██║  ██║██╔══██║██╔══██╗╚════██║██║██║╚██╔╝██║ ██╔██╗ 
    ██║  ██║██║  ██║██████╔╝██║  ██║██║  ██║███████║██║██║ ╚═╝ ██║██╔╝ ██╗
    ╚═╝  ╚═╝╚═╝  ╚═╝╚═════╝ ╚═╝  ╚═╝╚═╝  ╚═╝╚══════╝╚═╝╚═╝     ╚═╝╚═╝  ╚═╝

"""

import numpy as np


[docs] class Transmitter: """ A class defines basic parameters of a radar transmitter :param f: Waveform frequency (Hz). If ``f`` is a single number, radar transmits a single-tone waveform. For linear modulation, specify ``f`` with ``[f_start, f_stop]``. ``f`` can alse be a 1-D array of an arbitrary waveform, specify the time with ``t``. :type f: float or numpy.1darray :param t: Timing of each pulse (s). :type t: float or numpy.1darray :param float tx_power: Transmitter power (dBm) :param int pulses: Total number of pulses :param float prp: Pulse repetition period (s). ``prp >= pulse_length``. If it is ``None``, ``prp = pulse_length``. ``prp`` can alse be a 1-D array to specify different repetition period for each pulse. In this case, the length of the 1-D array should equals to the length of ``pulses`` :type repetitions_period: float or numpy.1darray :param numpy.1darray f_offset: Frequency offset for each pulse (Hz). The length must be the same as ``pulses``. :param numpy.1darray pn_f: Frequency of the phase noise (Hz) :param numpy.1darray pn_power: Power of the phase noise (dB/Hz) :param list[dict] channels: Properties of transmitter channels [{ - **location** (*numpy.1darray*) -- 3D location of the channel [x, y, z] (m) - **polarization** (*numpy.1darray*) -- Antenna polarization [x, y, z]. ``default = [0, 0, 1] (vertical polarization)`` - **delay** (*float*) -- Transmit delay (s). ``default 0`` - **azimuth_angle** (*numpy.1darray*) -- Angles for azimuth pattern (deg). ``default [-90, 90]`` - **azimuth_pattern** (*numpy.1darray*) -- Azimuth pattern (dB). ``default [0, 0]`` - **elevation_angle** (*numpy.1darray*) -- Angles for elevation pattern (deg). ``default [-90, 90]`` - **elevation_pattern** (*numpy.1darray*) -- Elevation pattern (dB). ``default [0, 0]`` - **pulse_amp** (*numpy.1darray*) -- Relative amplitude sequence for pulse's amplitude modulation. The array length should be the same as `pulses`. ``default 1`` - **pulse_phs** (*numpy.1darray*) -- Phase code sequence for pulse's phase modulation (deg). The array length should be the same as `pulses`. ``default 0`` - **mod_t** (*numpy.1darray*) -- Time stamps for waveform modulation (s). ``default None`` - **phs** (*numpy.1darray*) -- Phase scheme for waveform modulation (deg). ``default None`` - **amp** (*numpy.1darray*) -- Relative amplitude scheme for waveform modulation. ``default None`` }] :ivar dict rf_prop: RF properties - **tx_power**: Transmitter power (dBm) - **pn_f**: Frequency of the phase noise (Hz) - **pn_power**: Power of the phase noise (dB/Hz) :ivar dict waveform_prop: Waveform properties - **f**: Waveform frequency (Hz) - **t**: Timing of each pulse (s) - **bandwidth**: Transmitting bandwidth (Hz) - **pulse_length**: Transmitting length (s) - **pulses**: Number of pulses - **f_offset**: Frequency offset for each pulse - **prp**: Pulse repetition time (s) - **pulse_start_time**: Start time of each pulse :ivar dict txchannel_prop: Transmitter channels - **size**: Number of transmitter channels - **delay**: Tx start delay (s) - **grid**: Ray tracing grid size (deg) - **locations**: Location of the Tx channel [x, y, z] m - **polarization**: Polarization of the Tx channel - **waveform_mod**: Waveform modulation parameters - **pulse_mod**: Pulse modulation parameters - **az_angles**: Azimuth angles (deg) - **az_patterns**: Azimuth pattern (dB) - **el_angles**: Elevation angles (deg) - **el_patterns**: Elevation pattern (dB) - **antenna_gains**: Tx antenna gain (dB) **Waveform** :: █ █ █ █ █ █ █ █ █ █ █ █ █ █ █ █ █ █ █ █ █ █ █ █ █ █ █ █ █ █ █ █ █ █ █ █ █ █ prp █ █ +-----------+ █ █ +---f[1]---------> / / / █ █ / / / █ █ / / / █ █ / / / █ █ / / / ... █ █ / / / █ █ / / / █ █ / / / █ █ +---f[0]--->/ / / █ █ +-------+ █ █ t[0] t[1] █ █ █ █ Pulse +--------------------------------------+ █ █ modulation |pulse_amp[0]|pulse_amp[1]|pulse_amp[2]| ... █ █ |pulse_phs[0]|pulse_phs[1]|pulse_phs[2]| ... █ █ +--------------------------------------+ █ █ █ █ Waveform +--------------------------------------+ █ █ modulation | amp / phs / mod_t | ... █ █ +--------------------------------------+ █ █ █ █ █ █ █ █ █ █ █ █ █ █ █ █ █ █ █ █ █ █ █ █ █ █ █ █ █ █ █ █ █ █ █ █ █ █ """ def __init__( # pylint: disable=too-many-arguments self, f, t, tx_power=0, pulses=1, prp=None, f_offset=None, pn_f=None, pn_power=None, channels=None, ): self.rf_prop = {} self.waveform_prop = {} self.txchannel_prop = {} self.rf_prop["tx_power"] = tx_power self.rf_prop["pn_f"] = pn_f self.rf_prop["pn_power"] = pn_power self.validate_rf_prop(self.rf_prop) # get `f(t)` # the lenght of `f` should be the same as `t` if isinstance(f, (list, tuple, np.ndarray)): f = np.array(f) else: f = np.array([f, f]) if isinstance(t, (list, tuple, np.ndarray)): t = np.array(t) - t[0] else: t = np.array([0, t]) self.waveform_prop["f"] = f self.waveform_prop["t"] = t self.waveform_prop["bandwidth"] = np.max(f) - np.min(f) self.waveform_prop["pulse_length"] = t[-1] self.waveform_prop["pulses"] = pulses # frequency offset for each pulse # the length of `f_offset` should be the same as `pulses` if f_offset is None: f_offset = np.zeros(pulses) else: if isinstance(f_offset, (list, tuple, np.ndarray)): f_offset = np.array(f_offset) else: f_offset = f_offset + np.zeros(pulses) self.waveform_prop["f_offset"] = f_offset # Extend `prp` to a numpy.1darray. # Length equels to `pulses` if prp is None: prp = self.waveform_prop["pulse_length"] + np.zeros(pulses) else: if isinstance(prp, (list, tuple, np.ndarray)): prp = np.array(prp) else: prp = prp + np.zeros(pulses) self.waveform_prop["prp"] = prp # start time of each pulse, without considering the delay self.waveform_prop["pulse_start_time"] = np.cumsum(prp) - prp[0] self.validate_waveform_prop(self.waveform_prop) if channels is None: channels = [{"location": (0, 0, 0)}] self.txchannel_prop = self.process_txchannel_prop(channels) def validate_rf_prop(self, rf_prop): """ Validate RF properties :param dict rf_prop: RF properties :raises ValueError: Lengths of `pn_f` and `pn_power` should be the same :raises ValueError: Lengths of `pn_f` and `pn_power` should be the same :raises ValueError: Lengths of `pn_f` and `pn_power` should be the same """ if rf_prop["pn_f"] is not None and rf_prop["pn_power"] is None: raise ValueError("Lengths of `pn_f` and `pn_power` should be the same") if rf_prop["pn_f"] is None and rf_prop["pn_power"] is not None: raise ValueError("Lengths of `pn_f` and `pn_power` should be the same") if rf_prop["pn_f"] is not None and rf_prop["pn_power"] is not None: if len(rf_prop["pn_f"]) != len(rf_prop["pn_power"]): raise ValueError("Lengths of `pn_f` and `pn_power` should be the same") def validate_waveform_prop(self, waveform_prop): """ Validate waveform properties :param waveform_prop (dict): Wavefrom properties :raises ValueError: Lengths of `f` and `t` should be the same :raises ValueError: Lengths of `f_offset` and `pulses` should be the same :raises ValueError: Length of `prp` should equal to the length of `pulses` :raises ValueError: `prp` should be larger than `pulse_length` """ if len(waveform_prop["f"]) != len(waveform_prop["t"]): raise ValueError("Lengths of `f` and `t` should be the same") if len(waveform_prop["f_offset"]) != waveform_prop["pulses"]: raise ValueError("Lengths of `f_offset` and `pulses` should be the same") if len(waveform_prop["prp"]) != waveform_prop["pulses"]: raise ValueError("Length of `prp` should equal to the length of `pulses`") if np.min(waveform_prop["prp"]) < waveform_prop["pulse_length"]: raise ValueError("`prp` should be larger than `pulse_length`") def process_waveform_modulation(self, mod_t, amp, phs): """ Process waveform modulation parameters :param numpy.1darray mod_t: Time stamps for waveform modulation (s). ``default None`` :param numpy.1darray amp: Relative amplitude scheme for waveform modulation. ``default None`` :param numpy.1darray phs: Phase scheme for waveform modulation (deg). ``default None`` :raises ValueError: Lengths of `amp` and `phs` should be the same :raises ValueError: Lengths of `mod_t`, `amp`, and `phs` should be the same :return: Waveform modulation :rtype: dict """ if phs is not None and amp is None: amp = np.ones_like(phs) elif phs is None and amp is not None: phs = np.zeros_like(amp) if mod_t is None or amp is None or phs is None: return {"enabled": False, "var": None, "t": None} if isinstance(amp, (list, tuple, np.ndarray)): amp = np.array(amp) else: amp = np.array([amp, amp]) if isinstance(phs, (list, tuple, np.ndarray)): phs = np.array(phs) else: phs = np.array([phs, phs]) if isinstance(mod_t, (list, tuple, np.ndarray)): mod_t = np.array(mod_t) else: mod_t = np.array([0, mod_t]) if len(amp) != len(phs): raise ValueError("Lengths of `amp` and `phs` should be the same") mod_var = amp * np.exp(1j * phs / 180 * np.pi) if len(mod_t) != len(mod_var): raise ValueError("Lengths of `mod_t`, `amp`, and `phs` should be the same") return {"enabled": True, "var": mod_var, "t": mod_t} def process_pulse_modulation(self, pulse_amp, pulse_phs): """ Process pulse modulation parameters :param numpy.1darray pulse_amp: Relative amplitude sequence for pulse's amplitude modulation. The array length should be the same as `pulses`. ``default 1`` :param numpy.1darray pulse_phs: Phase code sequence for pulse's phase modulation (deg). The array length should be the same as `pulses`. ``default 0`` :raises ValueError: Lengths of `pulse_amp` and `pulses` should be the same :raises ValueError: Length of `pulse_phs` and `pulses` should be the same :return: Pulse modulation array :rtype: numpy.1darray """ if len(pulse_amp) != self.waveform_prop["pulses"]: raise ValueError("Lengths of `pulse_amp` and `pulses` should be the same") if len(pulse_phs) != self.waveform_prop["pulses"]: raise ValueError("Length of `pulse_phs` and `pulses` should be the same") return pulse_amp * np.exp(1j * (pulse_phs / 180 * np.pi)) def process_txchannel_prop(self, channels): """ Process transmitter channel parameters :param dict channels: Dictionary of transmitter channels :raises ValueError: Lengths of `azimuth_angle` and `azimuth_pattern` should be the same :raises ValueError: Lengths of `elevation_angle` and `elevation_pattern` should be the same :return: Transmitter channel properties :rtype: dict """ # number of transmitter channels txch_prop = {} txch_prop["size"] = len(channels) # firing delay for each channel txch_prop["delay"] = np.zeros(txch_prop["size"]) txch_prop["grid"] = np.zeros(txch_prop["size"]) txch_prop["locations"] = np.zeros((txch_prop["size"], 3)) txch_prop["polarization"] = np.zeros((txch_prop["size"], 3)) # waveform modulation parameters txch_prop["waveform_mod"] = [] # pulse modulation parameters txch_prop["pulse_mod"] = np.ones( (txch_prop["size"], self.waveform_prop["pulses"]), dtype=complex ) # azimuth patterns txch_prop["az_patterns"] = [] txch_prop["az_angles"] = [] # elevation patterns txch_prop["el_patterns"] = [] txch_prop["el_angles"] = [] # antenna peak gain # antenna gain is calculated based on azimuth pattern txch_prop["antenna_gains"] = np.zeros((txch_prop["size"])) for tx_idx, tx_element in enumerate(channels): txch_prop["delay"][tx_idx] = tx_element.get("delay", 0) txch_prop["grid"][tx_idx] = tx_element.get("grid", 1) txch_prop["locations"][tx_idx, :] = np.array(tx_element.get("location")) txch_prop["polarization"][tx_idx, :] = np.array( tx_element.get("polarization", [0, 0, 1]) ) txch_prop["waveform_mod"].append( self.process_waveform_modulation( tx_element.get("mod_t", None), tx_element.get("amp", None), tx_element.get("phs", None), ) ) txch_prop["pulse_mod"][tx_idx, :] = self.process_pulse_modulation( tx_element.get("pulse_amp", np.ones((self.waveform_prop["pulses"]))), tx_element.get("pulse_phs", np.zeros((self.waveform_prop["pulses"]))), ) # azimuth pattern az_angle = np.array(tx_element.get("azimuth_angle", [-90, 90])) az_pattern = np.array(tx_element.get("azimuth_pattern", [0, 0])) if len(az_angle) != len(az_pattern): raise ValueError( "Lengths of `azimuth_angle` and `azimuth_pattern` \ should be the same" ) txch_prop["antenna_gains"][tx_idx] = np.max(az_pattern) az_pattern = az_pattern - txch_prop["antenna_gains"][tx_idx] txch_prop["az_angles"].append(az_angle) txch_prop["az_patterns"].append(az_pattern) # elevation pattern el_angle = np.array(tx_element.get("elevation_angle", [-90, 90])) el_pattern = np.array(tx_element.get("elevation_pattern", [0, 0])) if len(el_angle) != len(el_pattern): raise ValueError( "Lengths of `elevation_angle` and `elevation_pattern` \ should be the same" ) el_pattern = el_pattern - np.max(el_pattern) txch_prop["el_angles"].append(el_angle) txch_prop["el_patterns"].append(el_pattern) return txch_prop