From 9f8c6b2235058b354747228081f928debde6806a Mon Sep 17 00:00:00 2001 From: Q Xu Date: Wed, 13 Nov 2024 16:57:42 +0800 Subject: [PATCH] add option to generate board signal with no overlapping bands --- doa_py/__init__.py | 2 +- doa_py/algorithm/broadband.py | 2 - doa_py/arrays.py | 43 +++++++++++++++---- doa_py/signals.py | 78 +++++++++++++++++++++++++++++++---- pyproject.toml | 2 +- 5 files changed, 107 insertions(+), 20 deletions(-) diff --git a/doa_py/__init__.py b/doa_py/__init__.py index c8127a3..6c348af 100644 --- a/doa_py/__init__.py +++ b/doa_py/__init__.py @@ -113,5 +113,5 @@ file for details. """ -__version__ = "0.2.2" +__version__ = "0.2.3" __author__ = "Qian Xu" diff --git a/doa_py/algorithm/broadband.py b/doa_py/algorithm/broadband.py index 80d0cd2..a50f2bd 100644 --- a/doa_py/algorithm/broadband.py +++ b/doa_py/algorithm/broadband.py @@ -144,7 +144,6 @@ def cssm( if fre_ref is None: # Find the frequency point with the maximum power fre_ref = fre_bins[np.argmax(np.abs(signal_fre_bins).sum(axis=0))] - print(fre_ref) # Calculate the manifold matrix corresponding to the pre-estimated angles at # the reference frequency point @@ -214,7 +213,6 @@ def tops( if fre_ref is None: fre_ref = fre_bins[np.argmax(np.abs(signal_fre_bins).sum(axis=(0, 2)))] - print(fre_ref) # index of reference frequency in FFT output ref_index = int(fre_ref / (fs / fre_bins.size)) diff --git a/doa_py/arrays.py b/doa_py/arrays.py index a92b9c4..859ef2a 100644 --- a/doa_py/arrays.py +++ b/doa_py/arrays.py @@ -2,7 +2,7 @@ import numpy as np -from .signals import BroadSignal +from .signals import BroadSignal, NarrowSignal, Signal C = 3e8 # wave speed @@ -88,11 +88,13 @@ def steering_vector(self, fre, angle_incidence, unit="deg"): def received_signal( self, - signal, + signal: Signal, angle_incidence, snr=None, nsamples=100, amp=None, + min_length_ratio=0.1, + no_overlap=False, unit="deg", ): """Generate array received signal based on array signal model @@ -110,6 +112,10 @@ def received_signal( snr: Signal-to-noise ratio. If set to None, no noise will be added nsamples (int): Number of snapshots, defaults to 100 amp: The amplitude of each signal, 1d numpy array + min_length_ratio (float): Minimum length ratio of the frequency + range in (f_max - f_min) + no_overlap (bool): If True, generate signals with non-overlapping + subbands unit: The unit of the angle, `rad` represents radian, `deg` represents degree. Defaults to 'deg'. """ @@ -118,16 +124,24 @@ def received_signal( if isinstance(signal, BroadSignal): received = self._gen_broadband( - signal, snr, nsamples, angle_incidence, amp + signal, + snr, + nsamples, + angle_incidence, + amp, + min_length_ratio, + no_overlap, ) - else: + if isinstance(signal, NarrowSignal): received = self._gen_narrowband( signal, snr, nsamples, angle_incidence, amp ) return received - def _gen_narrowband(self, signal, snr, nsamples, angle_incidence, amp): + def _gen_narrowband( + self, signal: NarrowSignal, snr, nsamples, angle_incidence, amp + ): """Generate narrowband received signal `azimuth` and `elevation` are already in radians @@ -150,7 +164,16 @@ def _gen_narrowband(self, signal, snr, nsamples, angle_incidence, amp): return received - def _gen_broadband(self, signal, snr, nsamples, angle_incidence, amp): + def _gen_broadband( + self, + signal: BroadSignal, + snr, + nsamples, + angle_incidence, + amp, + min_length_ratio=0.1, + no_overlap=False, + ): """Generate broadband received signal `azimuth` and `elevation` are already in radians @@ -162,7 +185,13 @@ def _gen_broadband(self, signal, snr, nsamples, angle_incidence, amp): num_antennas = self._element_position.shape[0] - incidence_signal = signal.gen(n=num_signal, nsamples=nsamples, amp=amp) + incidence_signal = signal.gen( + n=num_signal, + nsamples=nsamples, + min_length_ratio=min_length_ratio, + no_overlap=no_overlap, + amp=amp, + ) # generate array signal in frequency domain signal_fre_domain = np.fft.fft(incidence_signal, axis=1) diff --git a/doa_py/signals.py b/doa_py/signals.py index 89c7429..b523756 100644 --- a/doa_py/signals.py +++ b/doa_py/signals.py @@ -150,10 +150,28 @@ def fs(self): return self._fs @abstractmethod - def gen(self, n, nsamples, amp=None): + def gen( + self, n, nsamples, min_length_ratio=0.1, no_overlap=False, amp=None + ): + """Generate sampled signals + + Args: + n (int): Number of signals + nsamples (int): Number of snapshots + min_length_ratio (float): Minimum length ratio of the frequency + range in (f_max - f_min) + no_overlap (bool): If True, generate signals with non-overlapping + subbands + amp (np.array): Amplitude of the signals (1D array of size n), used + to define different amplitudes for different signals. + By default it will generate equal amplitude signal. + + Returns: + signal (np.array): Sampled signals + """ raise NotImplementedError - def _gen_fre_ranges(self, n, min_length_ratio=0.3): + def _gen_fre_bands(self, n, min_length_ratio=0.1): """Generate frequency ranges for each boardband signal Args: @@ -166,12 +184,41 @@ def _gen_fre_ranges(self, n, min_length_ratio=0.3): (n, 2) """ min_length = (self._f_max - self._f_min) * min_length_ratio - ranges = np.zeros((n, 2)) + bands = np.zeros((n, 2)) for i in range(n): length = self._rng.uniform(min_length, self._f_max - self._f_min) start = self._rng.uniform(self._f_min, self._f_max - length) - ranges[i] = [start, start + length] - return ranges + bands[i] = [start, start + length] + return bands + + def _generate_non_overlapping_bands(self, n, min_length_ratio=0.1): + """Generate n non-overlapping frequency bands within a specified range. + + Args: + n (int): Number of non-overlapping bands to generate. + min_length_ratio (float): Minimum length of each band as a ratio of + the maximum possible length. + + Returns: + np.ndarray: An array of shape (n, 2) where each row represents a + frequency band [start, end]. + """ + max_length = (self._f_max - self._f_min) // n + min_length = max_length * min_length_ratio + + bands = np.zeros((n, 2)) + + for i in range(n): + length = self._rng.uniform(min_length, max_length) + start = self._rng.uniform( + self._f_min + i * max_length, + self._f_min + (i + 1) * max_length - length, + ) + new_band = [start, start + length] + + bands[i] = new_band + + return bands class ChirpSignal(BroadSignal): @@ -187,9 +234,15 @@ def __init__(self, f_min, f_max, fs, rng=None): """ super().__init__(f_min, f_max, fs, rng=rng) - def gen(self, n, nsamples, amp=None): + def gen( + self, n, nsamples, min_length_ratio=0.1, no_overlap=False, amp=None + ): amp = self._get_amp(amp, n) - fre_ranges = self._gen_fre_ranges(n) + if no_overlap: + fre_ranges = self._generate_non_overlapping_bands( + n, min_length_ratio + ) + fre_ranges = self._gen_fre_bands(n, min_length_ratio) t = np.arange(nsamples) * 1 / self._fs f0 = fre_ranges[:, 0] @@ -222,9 +275,16 @@ def __init__(self, f_min, f_max, fs, ncarriers=100, rng=None): super().__init__(f_min, f_max, fs, rng=rng) self._ncarriers = ncarriers - def gen(self, n, nsamples, amp=None): + def gen( + self, n, nsamples, min_length_ratio=0.1, no_overlap=False, amp=None + ): amp = self._get_amp(amp, n) - fre_ranges = self._gen_fre_ranges(n) + if no_overlap: + fre_ranges = self._generate_non_overlapping_bands( + n, min_length_ratio + ) + else: + fre_ranges = self._gen_fre_bands(n, min_length_ratio) # generate random carrier frequencies fres = self._rng.uniform( diff --git a/pyproject.toml b/pyproject.toml index dc6816e..b396776 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "doa_py" -version = "0.2.2" +version = "0.2.3" description = "DOA estimation algorithms implemented in Python" readme = "README.md" license = { text = "MIT" }