Skip to content

Commit f82d5af

Browse files
authored
Merge pull request #64 from godsic/feature/phase-mode-transformation
Add phase mode expansion transformation for UCA
2 parents 2b852fc + 0b16978 commit f82d5af

File tree

3 files changed

+132
-18
lines changed

3 files changed

+132
-18
lines changed

_UI/_web_interface/kraken_web_interface.py

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@
7979
import ini_checker
8080
from krakenSDR_receiver import ReceiverRTLSDR
8181
from krakenSDR_signal_processor import SignalProcessor
82+
from krakenSDR_signal_processor import xi
8283
import tooltips
8384

8485
DECORRELATION_OPTIONS = [
@@ -146,10 +147,12 @@ def __init__(self):
146147
self.ant_spacing_meters = float(dsp_settings.get("ant_spacing_meters", 0.5))
147148

148149
if self.module_signal_processor.DOA_ant_alignment == "UCA":
150+
self.module_signal_processor.DOA_UCA_radius_m = self.ant_spacing_meters
149151
# Convert RADIUS to INTERELEMENT SPACING
150152
inter_elem_spacing = (np.sqrt(2)*self.ant_spacing_meters*np.sqrt(1-np.cos(np.deg2rad(360/self.module_signal_processor.channel_number))))
151153
self.module_signal_processor.DOA_inter_elem_space = inter_elem_spacing / (300 / float(dsp_settings.get("center_freq", 100.0)))
152154
else:
155+
self.module_signal_processor.DOA_UCA_radius_m = np.Infinity
153156
self.module_signal_processor.DOA_inter_elem_space = self.ant_spacing_meters / (300 / float(dsp_settings.get("center_freq", 100.0)))
154157

155158
self.module_signal_processor.ula_direction = dsp_settings.get("ula_direction", "Both")
@@ -1043,6 +1046,8 @@ def generate_config_page_layout(webInterface_inst):
10431046
value=webInterface_inst.module_signal_processor.DOA_decorrelation_method, style={"display":"inline-block"}, className="field-body"),
10441047
], className="field"),
10451048

1049+
html.Div([html.Div("", id="uca_decorrelation_warning" , className="field", style={"color":"#f39c12"})]),
1050+
10461051
html.Div([html.Div("ULA Output Direction:", id="label_ula_direction" , className="field-label"),
10471052
dcc.Dropdown(id='ula_direction',
10481053
options=[
@@ -2369,6 +2374,7 @@ def toggle_custom_array_fields(toggle_value):
23692374
Output(component_id="ambiguity_warning", component_property='children'),
23702375
Output(component_id="doa_decorrelation_method", component_property="options"),
23712376
Output(component_id="doa_decorrelation_method", component_property="disabled"),
2377+
Output(component_id="uca_decorrelation_warning", component_property="children"),
23722378
Output(component_id="expected_num_of_sources", component_property="options"),
23732379
Output(component_id="expected_num_of_sources", component_property="disabled"),],
23742380
[Input(component_id ="placeholder_update_freq" , component_property='children'),
@@ -2393,11 +2399,13 @@ def update_dsp_params(update_freq, en_doa, doa_decorrelation_method, spacing_met
23932399
#webInterface_inst.module_signal_processor.DOA_inter_elem_space = webInterface_inst.ant_spacing_meters / wavelength
23942400

23952401
if ant_arrangement == "UCA":
2402+
webInterface_inst.module_signal_processor.DOA_UCA_radius_m = webInterface_inst.ant_spacing_meters
23962403
# Convert RADIUS to INTERELEMENT SPACING
23972404
inter_elem_spacing = (np.sqrt(2) * webInterface_inst.ant_spacing_meters * np.sqrt(
23982405
1 - np.cos(np.deg2rad(360 / webInterface_inst.module_signal_processor.channel_number))))
23992406
webInterface_inst.module_signal_processor.DOA_inter_elem_space = inter_elem_spacing / wavelength
24002407
else:
2408+
webInterface_inst.module_signal_processor.DOA_UCA_radius_m = np.Infinity
24012409
webInterface_inst.module_signal_processor.DOA_inter_elem_space = webInterface_inst.ant_spacing_meters / wavelength
24022410

24032411
ant_spacing_wavelength = round(webInterface_inst.module_signal_processor.DOA_inter_elem_space, 3)
@@ -2439,15 +2447,30 @@ def update_dsp_params(update_freq, en_doa, doa_decorrelation_method, spacing_met
24392447

24402448
webInterface_inst.module_signal_processor.DOA_algorithm = doa_method
24412449

2442-
webInterface_inst.module_signal_processor.DOA_decorrelation_method = doa_decorrelation_method if ant_arrangement == "ULA" else DECORRELATION_OPTIONS[
2450+
is_odd_number_of_channels = (
2451+
webInterface_inst.module_signal_processor.channel_number % 2 != 0)
2452+
is_decorrelation_applicable = (ant_arrangement != "Custom" and is_odd_number_of_channels)
2453+
# UCA->VULA transformation works best if we have odd number of channels
2454+
webInterface_inst.module_signal_processor.DOA_decorrelation_method = doa_decorrelation_method if is_decorrelation_applicable else DECORRELATION_OPTIONS[
24432455
0]['value']
24442456

2445-
doa_decorrelation_method_options = DECORRELATION_OPTIONS if ant_arrangement == "ULA" else [
2457+
doa_decorrelation_method_options = DECORRELATION_OPTIONS if is_decorrelation_applicable else [
24462458
{
24472459
**DECORRELATION_OPTION, 'label': 'N/A'
24482460
} for DECORRELATION_OPTION in DECORRELATION_OPTIONS
24492461
]
2450-
doa_decorrelation_method_state = False if ant_arrangement == "ULA" else True
2462+
doa_decorrelation_method_state = False if is_decorrelation_applicable else True
2463+
2464+
if ant_arrangement == "UCA" and webInterface_inst.module_signal_processor.DOA_decorrelation_method != DECORRELATION_OPTIONS[0]['value']:
2465+
uca_decorrelation_warning = "WARNING: Using decorrelation methods with UCA array is still experimental as it might produce inconsistent results."
2466+
_, L = xi(webInterface_inst.ant_spacing_meters, webInterface_inst.daq_center_freq * 1.0e6)
2467+
M = webInterface_inst.module_signal_processor.channel_number // 2
2468+
if L < M:
2469+
if ambiguity_warning != "":
2470+
ambiguity_warning += "\n"
2471+
ambiguity_warning += "WARNING: If decorrelation is used with UCA, please try to keep radius of the array as large as possible."
2472+
else:
2473+
uca_decorrelation_warning = ""
24512474

24522475
webInterface_inst.module_signal_processor.DOA_ant_alignment=ant_arrangement
24532476
webInterface_inst._doa_fig_type = doa_fig_type
@@ -2477,8 +2500,9 @@ def update_dsp_params(update_freq, en_doa, doa_decorrelation_method, spacing_met
24772500

24782501
return [
24792502
str(ant_spacing_wavelength), spacing_label, ambiguity_warning,
2480-
doa_decorrelation_method_options, doa_decorrelation_method_state, num_of_sources,
2481-
num_of_sources_state
2503+
doa_decorrelation_method_options, doa_decorrelation_method_state,
2504+
uca_decorrelation_warning,
2505+
num_of_sources, num_of_sources_state
24822506
]
24832507

24842508
@app.callback(

_UI/_web_interface/tooltips.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,8 +101,7 @@
101101
# ),
102102
# Enable F-B averaging
103103
dbc.Tooltip([
104-
html.P("Decorrelation methods that might improve performance of DoA estimation in multipath and (or) low SNR environments."),
105-
html.P("(Available only for ULA antenna arrays)")],
104+
html.P("Decorrelation methods that might improve performance of DoA estimation in multipath and (or) low SNR environments.")],
106105
target="label_decorrelation",
107106
placement="bottom",
108107
className="tooltip"

_signal_processing/krakenSDR_signal_processor.py

Lines changed: 102 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import threading
3030
import queue
3131
import math
32+
from typing import Tuple
3233
import xml.etree.ElementTree as ET
3334
import requests
3435
import json
@@ -96,6 +97,7 @@ def __init__(self, data_que, module_receiver, logging_level=10):
9697
# self.en_DOA_MUSIC = False
9798
self.DOA_algorithm = "MUSIC"
9899
self.DOA_offset = 0
100+
self.DOA_UCA_radius_m = np.Infinity
99101
self.DOA_inter_elem_space = 0.5
100102
self.DOA_ant_alignment = "ULA"
101103
self.ula_direction = "Both"
@@ -567,30 +569,59 @@ def estimate_DOA(self, processed_signal, vfo_freq):
567569
Estimates the direction of arrival of the received RF signal
568570
"""
569571

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+
570582
# 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]
572585

573586
if self.DOA_decorrelation_method == 'FBA':
574587
R = de.forward_backward_avg(R)
575588
elif self.DOA_decorrelation_method == 'TOEP':
576589
R = toeplitzify(R)
577590
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+
581601
elif self.DOA_decorrelation_method == 'FBTOEP':
582602
R = fb_toeplitz_reconstruction(R)
583603

584-
585604
M = R.shape[0]
586605
scanning_vectors = []
587606
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,
591616
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)
594625

595626
# DOA estimation
596627
if self.DOA_algorithm == "Bartlett": # self.en_DOA_Bartlett:
@@ -764,7 +795,7 @@ def get_recording_filesize(self):
764795
2) # Convert to MB
765796

766797

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]:
768799
R = 6372.795477598
769800
line_length = 100
770801
theta = math.radians(my_bearing + (360 - doa))
@@ -950,6 +981,54 @@ def DOA_MUSIC(R, scanning_vectors, signal_dimension, angle_resolution=1):
950981
return ADORT
951982

952983

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+
9531032
# Numba optimized version of pyArgus corr_matrix_estimate with "fast". About 2x faster on Pi4
9541033
# @njit(fastmath=True, cache=True)
9551034
def corr_matrix(X):
@@ -985,6 +1064,18 @@ def fb_toeplitz_reconstruction(R: np.ndarray) -> np.ndarray:
9851064
return 0.5 * (R_f + R_b.conj())
9861065

9871066

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+
9881079
# LRU cache memoize about 1000x faster.
9891080
@lru_cache(maxsize=32)
9901081
def gen_scanning_vectors(M, DOA_inter_elem_space, type, offset):

0 commit comments

Comments
 (0)