|
29 | 29 | import threading |
30 | 30 | import queue |
31 | 31 | import math |
| 32 | +from typing import Tuple |
32 | 33 | import xml.etree.ElementTree as ET |
33 | 34 | import requests |
34 | 35 | import json |
@@ -96,6 +97,7 @@ def __init__(self, data_que, module_receiver, logging_level=10): |
96 | 97 | # self.en_DOA_MUSIC = False |
97 | 98 | self.DOA_algorithm = "MUSIC" |
98 | 99 | self.DOA_offset = 0 |
| 100 | + self.DOA_UCA_radius_m = np.Infinity |
99 | 101 | self.DOA_inter_elem_space = 0.5 |
100 | 102 | self.DOA_ant_alignment = "ULA" |
101 | 103 | self.ula_direction = "Both" |
@@ -567,30 +569,59 @@ def estimate_DOA(self, processed_signal, vfo_freq): |
567 | 569 | Estimates the direction of arrival of the received RF signal |
568 | 570 | """ |
569 | 571 |
|
| 572 | + antennas_alignment = self.DOA_ant_alignment |
| 573 | + if self.DOA_decorrelation_method != 'Off' and antennas_alignment == "UCA": |
| 574 | + antennas_alignment = "VULA" |
| 575 | + |
| 576 | + if antennas_alignment == "VULA": |
| 577 | + processed_signal = transform_to_phase_mode_space(processed_signal, |
| 578 | + self.DOA_UCA_radius_m, vfo_freq) |
| 579 | + # no idea on why this fliping of direction is needed |
| 580 | + processed_signal = np.flip(processed_signal) |
| 581 | + |
570 | 582 | # Calculating spatial correlation matrix |
571 | | - R = corr_matrix(processed_signal) # de.corr_matrix_estimate(self.processed_signal.T, imp="fast") |
| 583 | + R = corr_matrix(processed_signal) |
| 584 | + M = R.shape[0] |
572 | 585 |
|
573 | 586 | if self.DOA_decorrelation_method == 'FBA': |
574 | 587 | R = de.forward_backward_avg(R) |
575 | 588 | elif self.DOA_decorrelation_method == 'TOEP': |
576 | 589 | R = toeplitzify(R) |
577 | 590 | elif self.DOA_decorrelation_method == 'FBSS': |
578 | | - R = de.spatial_smoothing(processed_signal.T, |
579 | | - self.channel_number - 1, |
580 | | - "forward-backward") |
| 591 | + # VULA must have odd number of elements after spatial averaging |
| 592 | + smoothing_degree = 2 if antennas_alignment == "VULA" else 1 |
| 593 | + subarray_size = M - smoothing_degree |
| 594 | + if subarray_size > 1: |
| 595 | + R = de.spatial_smoothing(processed_signal.T, subarray_size, |
| 596 | + "forward-backward") |
| 597 | + else: |
| 598 | + # Too few channels for spatial smoothing, skipping it. |
| 599 | + pass |
| 600 | + |
581 | 601 | elif self.DOA_decorrelation_method == 'FBTOEP': |
582 | 602 | R = fb_toeplitz_reconstruction(R) |
583 | 603 |
|
584 | | - |
585 | 604 | M = R.shape[0] |
586 | 605 | scanning_vectors = [] |
587 | 606 | frq_ratio = vfo_freq / self.module_receiver.daq_center_freq |
588 | | - if self.DOA_ant_alignment == "UCA" or self.DOA_ant_alignment == "ULA": |
589 | | - inter_element_spacing = self.DOA_inter_elem_space * frq_ratio |
590 | | - scanning_vectors = gen_scanning_vectors(M, inter_element_spacing, self.DOA_ant_alignment, |
| 607 | + inter_element_spacing = self.DOA_inter_elem_space * frq_ratio |
| 608 | + |
| 609 | + if antennas_alignment == "ULA": |
| 610 | + scanning_vectors = gen_scanning_vectors(M, inter_element_spacing, |
| 611 | + antennas_alignment, |
| 612 | + int(self.array_offset)) |
| 613 | + elif antennas_alignment == "UCA": |
| 614 | + scanning_vectors = gen_scanning_vectors(M, inter_element_spacing, |
| 615 | + antennas_alignment, |
591 | 616 | int(self.array_offset)) |
592 | | - elif self.DOA_ant_alignment == "Custom": |
593 | | - scanning_vectors = gen_scanning_vectors_custom(M, self.custom_array_x * frq_ratio, self.custom_array_y * frq_ratio) |
| 617 | + elif antennas_alignment == "VULA": |
| 618 | + L = R.shape[0] // 2 |
| 619 | + scanning_vectors = gen_scanning_vectors_phase_modes_space(L, |
| 620 | + self.array_offset) |
| 621 | + elif antennas_alignment == "Custom": |
| 622 | + scanning_vectors = gen_scanning_vectors_custom(M, |
| 623 | + self.custom_array_x * frq_ratio, |
| 624 | + self.custom_array_y * frq_ratio) |
594 | 625 |
|
595 | 626 | # DOA estimation |
596 | 627 | if self.DOA_algorithm == "Bartlett": # self.en_DOA_Bartlett: |
@@ -764,7 +795,7 @@ def get_recording_filesize(self): |
764 | 795 | 2) # Convert to MB |
765 | 796 |
|
766 | 797 |
|
767 | | -def calculate_end_lat_lng(s_lat: float, s_lng: float, doa: float, my_bearing: float) -> (float, float): |
| 798 | +def calculate_end_lat_lng(s_lat: float, s_lng: float, doa: float, my_bearing: float) -> Tuple[float, float]: |
768 | 799 | R = 6372.795477598 |
769 | 800 | line_length = 100 |
770 | 801 | theta = math.radians(my_bearing + (360 - doa)) |
@@ -950,6 +981,54 @@ def DOA_MUSIC(R, scanning_vectors, signal_dimension, angle_resolution=1): |
950 | 981 | return ADORT |
951 | 982 |
|
952 | 983 |
|
| 984 | +def xi(uca_radius_m: float, frequency_Hz: float) -> Tuple[float, int]: |
| 985 | + wavelength_m = scipy.constants.speed_of_light / frequency_Hz |
| 986 | + x = 2.0 * np.pi * uca_radius_m / wavelength_m |
| 987 | + L = int(np.floor(x)) |
| 988 | + return x, L |
| 989 | + |
| 990 | +# The phase mode excitation transformation |
| 991 | +# as introduced by A. H. Tewfik and W. Hong, |
| 992 | +# "On the application of uniform linear array bearing estimation techniques to uniform circular arrays", |
| 993 | +# in IEEE Transactions on Signal Processing, vol. 40, no. 4, pp. 1008-1011, April 1992, |
| 994 | +# doi: 10.1109/78.127980. |
| 995 | +@lru_cache(maxsize=32) |
| 996 | +def T(uca_radius_m: float, frequency_Hz: float, N: int) -> np.ndarray: |
| 997 | + x, L = xi(uca_radius_m, frequency_Hz) |
| 998 | + |
| 999 | + # J |
| 1000 | + J = np.diag([ |
| 1001 | + 1.0 / ((1j**v) * scipy.special.jv(v, x)) |
| 1002 | + for v in range(-L, L + 1, 1) |
| 1003 | + ]) |
| 1004 | + |
| 1005 | + # F |
| 1006 | + F = np.array([[np.exp(2.0j * np.pi * (m * n / N)) for n in range(0, N, 1)] |
| 1007 | + for m in range(-L, L + 1, 1)]) |
| 1008 | + |
| 1009 | + return (J @ F) / float(N) |
| 1010 | + |
| 1011 | + |
| 1012 | +# The so-called "prewhitening" |
| 1013 | +# applied to turn A into unitary transformation |
| 1014 | +def whiten(A: np.ndarray) -> np.ndarray: |
| 1015 | + A_H = A.conj().T |
| 1016 | + A_w = A @ A_H |
| 1017 | + A_w = scipy.linalg.fractional_matrix_power(A_w, -0.5) |
| 1018 | + return A_w @ A |
| 1019 | + |
| 1020 | + |
| 1021 | +# @njit(fastmath=True, cache=True) |
| 1022 | +def transform_to_phase_mode_space(signal: np.ndarray, uca_radius_m: float, |
| 1023 | + frequency_Hz: float) -> np.ndarray: |
| 1024 | + T_ = T(uca_radius_m, frequency_Hz, signal.shape[0]) |
| 1025 | + # apparently T is not unitary and would "color" the noise in the input signal |
| 1026 | + # thus prewhitening needs to be applied particularly to make MUSIC work |
| 1027 | + Tw = whiten(T_) |
| 1028 | + x = Tw @ signal |
| 1029 | + return x |
| 1030 | + |
| 1031 | + |
953 | 1032 | # Numba optimized version of pyArgus corr_matrix_estimate with "fast". About 2x faster on Pi4 |
954 | 1033 | # @njit(fastmath=True, cache=True) |
955 | 1034 | def corr_matrix(X): |
@@ -985,6 +1064,18 @@ def fb_toeplitz_reconstruction(R: np.ndarray) -> np.ndarray: |
985 | 1064 | return 0.5 * (R_f + R_b.conj()) |
986 | 1065 |
|
987 | 1066 |
|
| 1067 | +# LRU cache memoize about 1000x faster. |
| 1068 | +@lru_cache(maxsize=32) |
| 1069 | +def gen_scanning_vectors_phase_modes_space(L, offset): |
| 1070 | + thetas = np.deg2rad(np.linspace(0, 359, 360, dtype=float)) |
| 1071 | + M = np.arange(-L, L + 1, dtype=float) |
| 1072 | + scanning_vectors = np.zeros((M.size, thetas.size), dtype=np.complex64) |
| 1073 | + for i in range(thetas.size): |
| 1074 | + scanning_vectors[:, i] = np.exp(1.0j * M * (thetas[i] + offset)) |
| 1075 | + |
| 1076 | + return np.ascontiguousarray(scanning_vectors) |
| 1077 | + |
| 1078 | + |
988 | 1079 | # LRU cache memoize about 1000x faster. |
989 | 1080 | @lru_cache(maxsize=32) |
990 | 1081 | def gen_scanning_vectors(M, DOA_inter_elem_space, type, offset): |
|
0 commit comments